github.com/containers/podman/v4@v4.9.4/pkg/machine/config.go (about)

     1  //go:build amd64 || arm64
     2  // +build amd64 arm64
     3  
     4  package machine
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/containers/podman/v4/pkg/machine/compression"
    19  	"github.com/containers/podman/v4/pkg/machine/define"
    20  	"github.com/containers/storage/pkg/homedir"
    21  	"github.com/containers/storage/pkg/lockfile"
    22  	"github.com/sirupsen/logrus"
    23  )
    24  
    25  type InitOptions struct {
    26  	CPUS               uint64
    27  	DiskSize           uint64
    28  	IgnitionPath       string
    29  	ImagePath          string
    30  	Volumes            []string
    31  	VolumeDriver       string
    32  	IsDefault          bool
    33  	Memory             uint64
    34  	Name               string
    35  	TimeZone           string
    36  	URI                url.URL
    37  	Username           string
    38  	ReExec             bool
    39  	Rootful            bool
    40  	UID                string // uid of the user that called machine
    41  	UserModeNetworking *bool  // nil = use backend/system default, false = disable, true = enable
    42  	USBs               []string
    43  }
    44  
    45  type Status = string
    46  
    47  const (
    48  	// Running indicates the qemu vm is running.
    49  	Running Status = "running"
    50  	// Stopped indicates the vm has stopped.
    51  	Stopped Status = "stopped"
    52  	// Starting indicated the vm is in the process of starting
    53  	Starting Status = "starting"
    54  	// Unknown means the state is not known
    55  	Unknown            Status = "unknown"
    56  	DefaultMachineName string = "podman-machine-default"
    57  	apiUpTimeout              = 20 * time.Second
    58  )
    59  
    60  type RemoteConnectionType string
    61  
    62  var (
    63  	SSHRemoteConnection     RemoteConnectionType = "ssh"
    64  	DefaultIgnitionUserName                      = "core"
    65  	ForwarderBinaryName                          = "gvproxy"
    66  )
    67  
    68  type Download struct {
    69  	Arch                  string
    70  	Artifact              define.Artifact
    71  	CacheDir              string
    72  	CompressionType       compression.ImageCompression
    73  	DataDir               string
    74  	Format                define.ImageFormat
    75  	ImageName             string
    76  	LocalPath             string
    77  	LocalUncompressedFile string
    78  	Sha256sum             string
    79  	Size                  int64
    80  	URL                   *url.URL
    81  	VMKind                VMType
    82  	VMName                string
    83  }
    84  
    85  type ListOptions struct{}
    86  
    87  type ListResponse struct {
    88  	Name               string
    89  	CreatedAt          time.Time
    90  	LastUp             time.Time
    91  	Running            bool
    92  	Starting           bool
    93  	Stream             string
    94  	VMType             string
    95  	CPUs               uint64
    96  	Memory             uint64
    97  	DiskSize           uint64
    98  	Port               int
    99  	RemoteUsername     string
   100  	IdentityPath       string
   101  	UserModeNetworking bool
   102  }
   103  
   104  type SetOptions struct {
   105  	CPUs               *uint64
   106  	DiskSize           *uint64
   107  	Memory             *uint64
   108  	Rootful            *bool
   109  	UserModeNetworking *bool
   110  	USBs               *[]string
   111  }
   112  
   113  type SSHOptions struct {
   114  	Username string
   115  	Args     []string
   116  }
   117  
   118  type StartOptions struct {
   119  	NoInfo bool
   120  	Quiet  bool
   121  }
   122  
   123  type StopOptions struct{}
   124  
   125  type RemoveOptions struct {
   126  	Force        bool
   127  	SaveKeys     bool
   128  	SaveImage    bool
   129  	SaveIgnition bool
   130  }
   131  
   132  type InspectOptions struct{}
   133  
   134  type VM interface {
   135  	Init(opts InitOptions) (bool, error)
   136  	Inspect() (*InspectInfo, error)
   137  	Remove(name string, opts RemoveOptions) (string, func() error, error)
   138  	Set(name string, opts SetOptions) ([]error, error)
   139  	SSH(name string, opts SSHOptions) error
   140  	Start(name string, opts StartOptions) error
   141  	State(bypass bool) (Status, error)
   142  	Stop(name string, opts StopOptions) error
   143  }
   144  
   145  func GetLock(name string, vmtype VMType) (*lockfile.LockFile, error) {
   146  	// FIXME: there's a painful amount of `GetConfDir` calls scattered
   147  	// across the code base.  This should be done once and stored
   148  	// somewhere instead.
   149  	vmConfigDir, err := GetConfDir(vmtype)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	lockPath := filepath.Join(vmConfigDir, name+".lock")
   155  	lock, err := lockfile.GetLockFile(lockPath)
   156  	if err != nil {
   157  		return nil, fmt.Errorf("creating lockfile for VM: %w", err)
   158  	}
   159  
   160  	return lock, nil
   161  }
   162  
   163  type DistributionDownload interface {
   164  	HasUsableCache() (bool, error)
   165  	Get() *Download
   166  	CleanCache() error
   167  }
   168  type InspectInfo struct {
   169  	ConfigPath         define.VMFile
   170  	ConnectionInfo     ConnectionConfig
   171  	Created            time.Time
   172  	Image              ImageConfig
   173  	LastUp             time.Time
   174  	Name               string
   175  	Resources          ResourceConfig
   176  	SSHConfig          SSHConfig
   177  	State              Status
   178  	UserModeNetworking bool
   179  	Rootful            bool
   180  }
   181  
   182  func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL {
   183  	// TODO Should this function have input verification?
   184  	userInfo := url.User(userName)
   185  	uri := url.URL{
   186  		Scheme:     "ssh",
   187  		Opaque:     "",
   188  		User:       userInfo,
   189  		Host:       host,
   190  		Path:       path,
   191  		RawPath:    "",
   192  		ForceQuery: false,
   193  		RawQuery:   "",
   194  		Fragment:   "",
   195  	}
   196  	if len(port) > 0 {
   197  		uri.Host = net.JoinHostPort(uri.Hostname(), port)
   198  	}
   199  	return uri
   200  }
   201  
   202  // GetCacheDir returns the dir where VM images are downloaded into when pulled
   203  func GetCacheDir(vmType VMType) (string, error) {
   204  	dataDir, err := GetDataDir(vmType)
   205  	if err != nil {
   206  		return "", err
   207  	}
   208  	cacheDir := filepath.Join(dataDir, "cache")
   209  	if _, err := os.Stat(cacheDir); !errors.Is(err, os.ErrNotExist) {
   210  		return cacheDir, nil
   211  	}
   212  	return cacheDir, os.MkdirAll(cacheDir, 0755)
   213  }
   214  
   215  // GetDataDir returns the filepath where vm images should
   216  // live for podman-machine.
   217  func GetDataDir(vmType VMType) (string, error) {
   218  	dataDirPrefix, err := DataDirPrefix()
   219  	if err != nil {
   220  		return "", err
   221  	}
   222  	dataDir := filepath.Join(dataDirPrefix, vmType.String())
   223  	if _, err := os.Stat(dataDir); !errors.Is(err, os.ErrNotExist) {
   224  		return dataDir, nil
   225  	}
   226  	mkdirErr := os.MkdirAll(dataDir, 0755)
   227  	return dataDir, mkdirErr
   228  }
   229  
   230  // GetGLobalDataDir returns the root of all backends
   231  // for shared machine data.
   232  func GetGlobalDataDir() (string, error) {
   233  	dataDir, err := DataDirPrefix()
   234  	if err != nil {
   235  		return "", err
   236  	}
   237  
   238  	return dataDir, os.MkdirAll(dataDir, 0755)
   239  }
   240  
   241  // DataDirPrefix returns the path prefix for all machine data files
   242  func DataDirPrefix() (string, error) {
   243  	data, err := homedir.GetDataHome()
   244  	if err != nil {
   245  		return "", err
   246  	}
   247  	dataDir := filepath.Join(data, "containers", "podman", "machine")
   248  	return dataDir, nil
   249  }
   250  
   251  // GetConfigDir returns the filepath to where configuration
   252  // files for podman-machine should live
   253  func GetConfDir(vmType VMType) (string, error) {
   254  	confDirPrefix, err := ConfDirPrefix()
   255  	if err != nil {
   256  		return "", err
   257  	}
   258  	confDir := filepath.Join(confDirPrefix, vmType.String())
   259  	if _, err := os.Stat(confDir); !errors.Is(err, os.ErrNotExist) {
   260  		return confDir, nil
   261  	}
   262  	mkdirErr := os.MkdirAll(confDir, 0755)
   263  	return confDir, mkdirErr
   264  }
   265  
   266  // ConfDirPrefix returns the path prefix for all machine config files
   267  func ConfDirPrefix() (string, error) {
   268  	conf, err := homedir.GetConfigHome()
   269  	if err != nil {
   270  		return "", err
   271  	}
   272  	confDir := filepath.Join(conf, "containers", "podman", "machine")
   273  	return confDir, nil
   274  }
   275  
   276  type USBConfig struct {
   277  	Bus       string
   278  	DevNumber string
   279  	Vendor    int
   280  	Product   int
   281  }
   282  
   283  // ResourceConfig describes physical attributes of the machine
   284  type ResourceConfig struct {
   285  	// CPUs to be assigned to the VM
   286  	CPUs uint64
   287  	// Disk size in gigabytes assigned to the vm
   288  	DiskSize uint64
   289  	// Memory in megabytes assigned to the vm
   290  	Memory uint64
   291  	// Usbs
   292  	USBs []USBConfig
   293  }
   294  
   295  type Mount struct {
   296  	ReadOnly bool
   297  	Source   string
   298  	Tag      string
   299  	Target   string
   300  	Type     string
   301  }
   302  
   303  // ImageConfig describes the bootable image for the VM
   304  type ImageConfig struct {
   305  	// IgnitionFile is the path to the filesystem where the
   306  	// ignition file was written (if needs one)
   307  	IgnitionFile define.VMFile `json:"IgnitionFilePath"`
   308  	// ImageStream is the update stream for the image
   309  	ImageStream string
   310  	// ImageFile is the fq path to
   311  	ImagePath define.VMFile `json:"ImagePath"`
   312  }
   313  
   314  // HostUser describes the host user
   315  type HostUser struct {
   316  	// Whether this machine should run in a rootful or rootless manner
   317  	Rootful bool
   318  	// UID is the numerical id of the user that called machine
   319  	UID int
   320  	// Whether one of these fields has changed and actions should be taken
   321  	Modified bool `json:"HostUserModified"`
   322  }
   323  
   324  // SSHConfig contains remote access information for SSH
   325  type SSHConfig struct {
   326  	// IdentityPath is the fq path to the ssh priv key
   327  	IdentityPath string
   328  	// SSH port for user networking
   329  	Port int
   330  	// RemoteUsername of the vm user
   331  	RemoteUsername string
   332  }
   333  
   334  // ConnectionConfig contains connections like sockets, etc.
   335  type ConnectionConfig struct {
   336  	// PodmanSocket is the exported podman service socket
   337  	PodmanSocket *define.VMFile `json:"PodmanSocket"`
   338  	// PodmanPipe is the exported podman service named pipe (Windows hosts only)
   339  	PodmanPipe *define.VMFile `json:"PodmanPipe"`
   340  }
   341  
   342  type VMType int64
   343  
   344  const (
   345  	QemuVirt VMType = iota
   346  	WSLVirt
   347  	AppleHvVirt
   348  	HyperVVirt
   349  	UnknownVirt
   350  )
   351  
   352  func (v VMType) String() string {
   353  	switch v {
   354  	case WSLVirt:
   355  		return "wsl"
   356  	case AppleHvVirt:
   357  		return "applehv"
   358  	case HyperVVirt:
   359  		return "hyperv"
   360  	}
   361  	return "qemu"
   362  }
   363  
   364  type APIForwardingState int
   365  
   366  const (
   367  	NoForwarding APIForwardingState = iota
   368  	ClaimUnsupported
   369  	NotInstalled
   370  	MachineLocal
   371  	DockerGlobal
   372  )
   373  
   374  func ParseVMType(input string, emptyFallback VMType) (VMType, error) {
   375  	switch strings.TrimSpace(strings.ToLower(input)) {
   376  	case "qemu":
   377  		return QemuVirt, nil
   378  	case "wsl":
   379  		return WSLVirt, nil
   380  	case "applehv":
   381  		return AppleHvVirt, nil
   382  	case "hyperv":
   383  		return HyperVVirt, nil
   384  	case "":
   385  		return emptyFallback, nil
   386  	default:
   387  		return UnknownVirt, fmt.Errorf("unknown VMType `%s`", input)
   388  	}
   389  }
   390  
   391  type VirtProvider interface { //nolint:interfacebloat
   392  	Artifact() define.Artifact
   393  	CheckExclusiveActiveVM() (bool, string, error)
   394  	Compression() compression.ImageCompression
   395  	Format() define.ImageFormat
   396  	IsValidVMName(name string) (bool, error)
   397  	List(opts ListOptions) ([]*ListResponse, error)
   398  	LoadVMByName(name string) (VM, error)
   399  	NewMachine(opts InitOptions) (VM, error)
   400  	NewDownload(vmName string) (Download, error)
   401  	RemoveAndCleanMachines() error
   402  	VMType() VMType
   403  }
   404  
   405  type Virtualization struct {
   406  	artifact    define.Artifact
   407  	compression compression.ImageCompression
   408  	format      define.ImageFormat
   409  	vmKind      VMType
   410  }
   411  
   412  func (p *Virtualization) Artifact() define.Artifact {
   413  	return p.artifact
   414  }
   415  
   416  func (p *Virtualization) Compression() compression.ImageCompression {
   417  	return p.compression
   418  }
   419  
   420  func (p *Virtualization) Format() define.ImageFormat {
   421  	return p.format
   422  }
   423  
   424  func (p *Virtualization) VMType() VMType {
   425  	return p.vmKind
   426  }
   427  
   428  func (p *Virtualization) NewDownload(vmName string) (Download, error) {
   429  	cacheDir, err := GetCacheDir(p.VMType())
   430  	if err != nil {
   431  		return Download{}, err
   432  	}
   433  
   434  	dataDir, err := GetDataDir(p.VMType())
   435  	if err != nil {
   436  		return Download{}, err
   437  	}
   438  
   439  	return Download{
   440  		Artifact:        p.Artifact(),
   441  		CacheDir:        cacheDir,
   442  		CompressionType: p.Compression(),
   443  		DataDir:         dataDir,
   444  		Format:          p.Format(),
   445  		VMKind:          p.VMType(),
   446  		VMName:          vmName,
   447  	}, nil
   448  }
   449  
   450  func NewVirtualization(artifact define.Artifact, compression compression.ImageCompression, format define.ImageFormat, vmKind VMType) Virtualization {
   451  	return Virtualization{
   452  		artifact,
   453  		compression,
   454  		format,
   455  		vmKind,
   456  	}
   457  }
   458  
   459  func WaitAndPingAPI(sock string) {
   460  	client := http.Client{
   461  		Transport: &http.Transport{
   462  			DialContext: func(context.Context, string, string) (net.Conn, error) {
   463  				con, err := net.DialTimeout("unix", sock, apiUpTimeout)
   464  				if err != nil {
   465  					return nil, err
   466  				}
   467  				if err := con.SetDeadline(time.Now().Add(apiUpTimeout)); err != nil {
   468  					return nil, err
   469  				}
   470  				return con, nil
   471  			},
   472  		},
   473  	}
   474  
   475  	resp, err := client.Get("http://host/_ping")
   476  	if err == nil {
   477  		defer resp.Body.Close()
   478  	}
   479  	if err != nil || resp.StatusCode != 200 {
   480  		logrus.Warn("API socket failed ping test")
   481  	}
   482  }
   483  
   484  func (dl Download) NewFcosDownloader(imageStream FCOSStream) (DistributionDownload, error) {
   485  	info, err := dl.GetFCOSDownload(imageStream)
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  	urlSplit := strings.Split(info.Location, "/")
   490  	dl.ImageName = urlSplit[len(urlSplit)-1]
   491  	downloadURL, err := url.Parse(info.Location)
   492  	if err != nil {
   493  		return nil, err
   494  	}
   495  
   496  	// Complete the download struct
   497  	dl.Arch = GetFcosArch()
   498  	// This could be eliminated as a struct and be a generated()
   499  	dl.LocalPath = filepath.Join(dl.CacheDir, dl.ImageName)
   500  	dl.Sha256sum = info.Sha256Sum
   501  	dl.URL = downloadURL
   502  	fcd := FcosDownload{
   503  		Download: dl,
   504  	}
   505  	dataDir, err := GetDataDir(dl.VMKind)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  	fcd.Download.LocalUncompressedFile = fcd.GetLocalUncompressedFile(dataDir)
   510  	return fcd, nil
   511  }
   512  
   513  // AcquireVMImage determines if the image is already in a FCOS stream. If so,
   514  // retrieves the image path of the uncompressed file. Otherwise, the user has
   515  // provided an alternative image, so we set the image path and download the image.
   516  func (dl Download) AcquireVMImage(imagePath string) (*define.VMFile, FCOSStream, error) {
   517  	var (
   518  		err           error
   519  		imageLocation *define.VMFile
   520  		fcosStream    FCOSStream
   521  	)
   522  
   523  	switch imagePath {
   524  	// TODO these need to be re-typed as FCOSStreams
   525  	case Testing.String(), Next.String(), Stable.String(), "":
   526  		// Get image as usual
   527  		fcosStream, err = FCOSStreamFromString(imagePath)
   528  		if err != nil {
   529  			return nil, 0, err
   530  		}
   531  
   532  		dd, err := dl.NewFcosDownloader(fcosStream)
   533  		if err != nil {
   534  			return nil, 0, err
   535  		}
   536  
   537  		imageLocation, err = define.NewMachineFile(dd.Get().LocalUncompressedFile, nil)
   538  		if err != nil {
   539  			return nil, 0, err
   540  		}
   541  
   542  		if err := DownloadImage(dd); err != nil {
   543  			return nil, 0, err
   544  		}
   545  	default:
   546  		// The user has provided an alternate image which can be a file path
   547  		// or URL.
   548  		fcosStream = CustomStream
   549  		imgPath, err := dl.AcquireAlternateImage(imagePath)
   550  		if err != nil {
   551  			return nil, 0, err
   552  		}
   553  		imageLocation = imgPath
   554  	}
   555  	return imageLocation, fcosStream, nil
   556  }