//go:build amd64 || arm64 // +build amd64 arm64 package machine import ( "context" "errors" "fmt" "net" "net/http" "net/url" "os" "path/filepath" "strings" "time" "github.com/containers/podman/v4/pkg/machine/compression" "github.com/containers/podman/v4/pkg/machine/define" "github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/lockfile" "github.com/sirupsen/logrus" ) type InitOptions struct { CPUS uint64 DiskSize uint64 IgnitionPath string ImagePath string Volumes []string VolumeDriver string IsDefault bool Memory uint64 Name string TimeZone string URI url.URL Username string ReExec bool Rootful bool UID string // uid of the user that called machine UserModeNetworking *bool // nil = use backend/system default, false = disable, true = enable USBs []string } type Status = string const ( // Running indicates the qemu vm is running. Running Status = "running" // Stopped indicates the vm has stopped. Stopped Status = "stopped" // Starting indicated the vm is in the process of starting Starting Status = "starting" // Unknown means the state is not known Unknown Status = "unknown" DefaultMachineName string = "podman-machine-default" apiUpTimeout = 20 * time.Second ) type RemoteConnectionType string var ( SSHRemoteConnection RemoteConnectionType = "ssh" DefaultIgnitionUserName = "core" ForwarderBinaryName = "gvproxy" ) type Download struct { Arch string Artifact define.Artifact CacheDir string CompressionType compression.ImageCompression DataDir string Format define.ImageFormat ImageName string LocalPath string LocalUncompressedFile string Sha256sum string Size int64 URL *url.URL VMKind VMType VMName string } type ListOptions struct{} type ListResponse struct { Name string CreatedAt time.Time LastUp time.Time Running bool Starting bool Stream string VMType string CPUs uint64 Memory uint64 DiskSize uint64 Port int RemoteUsername string IdentityPath string UserModeNetworking bool } type SetOptions struct { CPUs *uint64 DiskSize *uint64 Memory *uint64 Rootful *bool UserModeNetworking *bool USBs *[]string } type SSHOptions struct { Username string Args []string } type StartOptions struct { NoInfo bool Quiet bool } type StopOptions struct{} type RemoveOptions struct { Force bool SaveKeys bool SaveImage bool SaveIgnition bool } type InspectOptions struct{} type VM interface { Init(opts InitOptions) (bool, error) Inspect() (*InspectInfo, error) Remove(name string, opts RemoveOptions) (string, func() error, error) Set(name string, opts SetOptions) ([]error, error) SSH(name string, opts SSHOptions) error Start(name string, opts StartOptions) error State(bypass bool) (Status, error) Stop(name string, opts StopOptions) error } func GetLock(name string, vmtype VMType) (*lockfile.LockFile, error) { // FIXME: there's a painful amount of `GetConfDir` calls scattered // across the code base. This should be done once and stored // somewhere instead. vmConfigDir, err := GetConfDir(vmtype) if err != nil { return nil, err } lockPath := filepath.Join(vmConfigDir, name+".lock") lock, err := lockfile.GetLockFile(lockPath) if err != nil { return nil, fmt.Errorf("creating lockfile for VM: %w", err) } return lock, nil } type DistributionDownload interface { HasUsableCache() (bool, error) Get() *Download CleanCache() error } type InspectInfo struct { ConfigPath define.VMFile ConnectionInfo ConnectionConfig Created time.Time Image ImageConfig LastUp time.Time Name string Resources ResourceConfig SSHConfig SSHConfig State Status UserModeNetworking bool Rootful bool } func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL { // TODO Should this function have input verification? userInfo := url.User(userName) uri := url.URL{ Scheme: "ssh", Opaque: "", User: userInfo, Host: host, Path: path, RawPath: "", ForceQuery: false, RawQuery: "", Fragment: "", } if len(port) > 0 { uri.Host = net.JoinHostPort(uri.Hostname(), port) } return uri } // GetCacheDir returns the dir where VM images are downloaded into when pulled func GetCacheDir(vmType VMType) (string, error) { dataDir, err := GetDataDir(vmType) if err != nil { return "", err } cacheDir := filepath.Join(dataDir, "cache") if _, err := os.Stat(cacheDir); !errors.Is(err, os.ErrNotExist) { return cacheDir, nil } return cacheDir, os.MkdirAll(cacheDir, 0755) } // GetDataDir returns the filepath where vm images should // live for podman-machine. func GetDataDir(vmType VMType) (string, error) { dataDirPrefix, err := DataDirPrefix() if err != nil { return "", err } dataDir := filepath.Join(dataDirPrefix, vmType.String()) if _, err := os.Stat(dataDir); !errors.Is(err, os.ErrNotExist) { return dataDir, nil } mkdirErr := os.MkdirAll(dataDir, 0755) return dataDir, mkdirErr } // GetGLobalDataDir returns the root of all backends // for shared machine data. func GetGlobalDataDir() (string, error) { dataDir, err := DataDirPrefix() if err != nil { return "", err } return dataDir, os.MkdirAll(dataDir, 0755) } // DataDirPrefix returns the path prefix for all machine data files func DataDirPrefix() (string, error) { data, err := homedir.GetDataHome() if err != nil { return "", err } dataDir := filepath.Join(data, "containers", "podman", "machine") return dataDir, nil } // GetConfigDir returns the filepath to where configuration // files for podman-machine should live func GetConfDir(vmType VMType) (string, error) { confDirPrefix, err := ConfDirPrefix() if err != nil { return "", err } confDir := filepath.Join(confDirPrefix, vmType.String()) if _, err := os.Stat(confDir); !errors.Is(err, os.ErrNotExist) { return confDir, nil } mkdirErr := os.MkdirAll(confDir, 0755) return confDir, mkdirErr } // ConfDirPrefix returns the path prefix for all machine config files func ConfDirPrefix() (string, error) { conf, err := homedir.GetConfigHome() if err != nil { return "", err } confDir := filepath.Join(conf, "containers", "podman", "machine") return confDir, nil } type USBConfig struct { Bus string DevNumber string Vendor int Product int } // ResourceConfig describes physical attributes of the machine type ResourceConfig struct { // CPUs to be assigned to the VM CPUs uint64 // Disk size in gigabytes assigned to the vm DiskSize uint64 // Memory in megabytes assigned to the vm Memory uint64 // Usbs USBs []USBConfig } type Mount struct { ReadOnly bool Source string Tag string Target string Type string } // ImageConfig describes the bootable image for the VM type ImageConfig struct { // IgnitionFile is the path to the filesystem where the // ignition file was written (if needs one) IgnitionFile define.VMFile `json:"IgnitionFilePath"` // ImageStream is the update stream for the image ImageStream string // ImageFile is the fq path to ImagePath define.VMFile `json:"ImagePath"` } // HostUser describes the host user type HostUser struct { // Whether this machine should run in a rootful or rootless manner Rootful bool // UID is the numerical id of the user that called machine UID int // Whether one of these fields has changed and actions should be taken Modified bool `json:"HostUserModified"` } // SSHConfig contains remote access information for SSH type SSHConfig struct { // IdentityPath is the fq path to the ssh priv key IdentityPath string // SSH port for user networking Port int // RemoteUsername of the vm user RemoteUsername string } // ConnectionConfig contains connections like sockets, etc. type ConnectionConfig struct { // PodmanSocket is the exported podman service socket PodmanSocket *define.VMFile `json:"PodmanSocket"` // PodmanPipe is the exported podman service named pipe (Windows hosts only) PodmanPipe *define.VMFile `json:"PodmanPipe"` } type VMType int64 const ( QemuVirt VMType = iota WSLVirt AppleHvVirt HyperVVirt UnknownVirt ) func (v VMType) String() string { switch v { case WSLVirt: return "wsl" case AppleHvVirt: return "applehv" case HyperVVirt: return "hyperv" } return "qemu" } type APIForwardingState int const ( NoForwarding APIForwardingState = iota ClaimUnsupported NotInstalled MachineLocal DockerGlobal ) func ParseVMType(input string, emptyFallback VMType) (VMType, error) { switch strings.TrimSpace(strings.ToLower(input)) { case "qemu": return QemuVirt, nil case "wsl": return WSLVirt, nil case "applehv": return AppleHvVirt, nil case "hyperv": return HyperVVirt, nil case "": return emptyFallback, nil default: return UnknownVirt, fmt.Errorf("unknown VMType `%s`", input) } } type VirtProvider interface { //nolint:interfacebloat Artifact() define.Artifact CheckExclusiveActiveVM() (bool, string, error) Compression() compression.ImageCompression Format() define.ImageFormat IsValidVMName(name string) (bool, error) List(opts ListOptions) ([]*ListResponse, error) LoadVMByName(name string) (VM, error) NewMachine(opts InitOptions) (VM, error) NewDownload(vmName string) (Download, error) RemoveAndCleanMachines() error VMType() VMType } type Virtualization struct { artifact define.Artifact compression compression.ImageCompression format define.ImageFormat vmKind VMType } func (p *Virtualization) Artifact() define.Artifact { return p.artifact } func (p *Virtualization) Compression() compression.ImageCompression { return p.compression } func (p *Virtualization) Format() define.ImageFormat { return p.format } func (p *Virtualization) VMType() VMType { return p.vmKind } func (p *Virtualization) NewDownload(vmName string) (Download, error) { cacheDir, err := GetCacheDir(p.VMType()) if err != nil { return Download{}, err } dataDir, err := GetDataDir(p.VMType()) if err != nil { return Download{}, err } return Download{ Artifact: p.Artifact(), CacheDir: cacheDir, CompressionType: p.Compression(), DataDir: dataDir, Format: p.Format(), VMKind: p.VMType(), VMName: vmName, }, nil } func NewVirtualization(artifact define.Artifact, compression compression.ImageCompression, format define.ImageFormat, vmKind VMType) Virtualization { return Virtualization{ artifact, compression, format, vmKind, } } func WaitAndPingAPI(sock string) { client := http.Client{ Transport: &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { con, err := net.DialTimeout("unix", sock, apiUpTimeout) if err != nil { return nil, err } if err := con.SetDeadline(time.Now().Add(apiUpTimeout)); err != nil { return nil, err } return con, nil }, }, } resp, err := client.Get("http://host/_ping") if err == nil { defer resp.Body.Close() } if err != nil || resp.StatusCode != 200 { logrus.Warn("API socket failed ping test") } } func (dl Download) NewFcosDownloader(imageStream FCOSStream) (DistributionDownload, error) { info, err := dl.GetFCOSDownload(imageStream) if err != nil { return nil, err } urlSplit := strings.Split(info.Location, "/") dl.ImageName = urlSplit[len(urlSplit)-1] downloadURL, err := url.Parse(info.Location) if err != nil { return nil, err } // Complete the download struct dl.Arch = GetFcosArch() // This could be eliminated as a struct and be a generated() dl.LocalPath = filepath.Join(dl.CacheDir, dl.ImageName) dl.Sha256sum = info.Sha256Sum dl.URL = downloadURL fcd := FcosDownload{ Download: dl, } dataDir, err := GetDataDir(dl.VMKind) if err != nil { return nil, err } fcd.Download.LocalUncompressedFile = fcd.GetLocalUncompressedFile(dataDir) return fcd, nil } // AcquireVMImage determines if the image is already in a FCOS stream. If so, // retrieves the image path of the uncompressed file. Otherwise, the user has // provided an alternative image, so we set the image path and download the image. func (dl Download) AcquireVMImage(imagePath string) (*define.VMFile, FCOSStream, error) { var ( err error imageLocation *define.VMFile fcosStream FCOSStream ) switch imagePath { // TODO these need to be re-typed as FCOSStreams case Testing.String(), Next.String(), Stable.String(), "": // Get image as usual fcosStream, err = FCOSStreamFromString(imagePath) if err != nil { return nil, 0, err } dd, err := dl.NewFcosDownloader(fcosStream) if err != nil { return nil, 0, err } imageLocation, err = define.NewMachineFile(dd.Get().LocalUncompressedFile, nil) if err != nil { return nil, 0, err } if err := DownloadImage(dd); err != nil { return nil, 0, err } default: // The user has provided an alternate image which can be a file path // or URL. fcosStream = CustomStream imgPath, err := dl.AcquireAlternateImage(imagePath) if err != nil { return nil, 0, err } imageLocation = imgPath } return imageLocation, fcosStream, nil }