mirror of https://github.com/containers/podman.git
557 lines
14 KiB
Go
557 lines
14 KiB
Go
//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
|
|
}
|