mirror of
https://github.com/containers/podman.git
synced 2025-08-06 19:44:14 +08:00

this pr represents a shift in how we download podman machine images. these images will now be stored in oci registry and will replace the default method of downloading an image. you can still use a reference to a disk image as a path or url too with the --image-path switch. the final registry and location of the images has not been determined; and will need to be updated in podman as well. i dont think we need to allow --image-path to accept a registry/image for the podman 5.0 release. i do think there will be demand for this. upgrades also need to be plumbed. for example, updating from an oci registry. once we make decisions on final image locations/registrties as well as some behaviors of init and the oci pull, we must update the machine-init documentation. Signed-off-by: Brent Baude <bbaude@redhat.com>
412 lines
9.6 KiB
Go
412 lines
9.6 KiB
Go
//go:build amd64 || arm64
|
|
|
|
package machine
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containers/podman/v5/pkg/machine/compression"
|
|
"github.com/containers/podman/v5/pkg/machine/define"
|
|
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
|
|
"github.com/containers/storage/pkg/homedir"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
DefaultMachineName string = "podman-machine-default"
|
|
apiUpTimeout = 20 * time.Second
|
|
)
|
|
|
|
var (
|
|
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 define.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 SSHOptions struct {
|
|
Username string
|
|
Args []string
|
|
}
|
|
|
|
type StartOptions struct {
|
|
NoInfo bool
|
|
Quiet bool
|
|
}
|
|
|
|
type StopOptions struct{}
|
|
|
|
type RemoveOptions struct {
|
|
Force bool
|
|
SaveImage bool
|
|
SaveIgnition bool
|
|
}
|
|
|
|
type InspectOptions struct{}
|
|
|
|
// TODO This can be removed when WSL is refactored into podman 5
|
|
type VM interface {
|
|
Init(opts define.InitOptions) (bool, error)
|
|
Inspect() (*InspectInfo, error)
|
|
Remove(name string, opts RemoveOptions) (string, func() error, error)
|
|
Set(name string, opts define.SetOptions) ([]error, error)
|
|
SSH(name string, opts SSHOptions) error
|
|
Start(name string, opts StartOptions) error
|
|
State(bypass bool) (define.Status, error)
|
|
Stop(name string, opts StopOptions) error
|
|
}
|
|
|
|
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 vmconfigs.ResourceConfig
|
|
SSHConfig vmconfigs.SSHConfig
|
|
State define.Status
|
|
UserModeNetworking bool
|
|
Rootful bool
|
|
}
|
|
|
|
// GetCacheDir returns the dir where VM images are downloaded into when pulled
|
|
func GetCacheDir(vmType define.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 define.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)
|
|
}
|
|
|
|
func GetMachineDirs(vmType define.VMType) (*define.MachineDirs, error) {
|
|
rtDir, err := getRuntimeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rtDir = filepath.Join(rtDir, "podman")
|
|
configDir, err := GetConfDir(vmType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configDirFile, err := define.NewMachineFile(configDir, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dataDir, err := GetDataDir(vmType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dataDirFile, err := define.NewMachineFile(dataDir, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
imageCacheDir, err := dataDirFile.AppendToNewVMFile("cache", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rtDirFile, err := define.NewMachineFile(rtDir, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dirs := define.MachineDirs{
|
|
ConfigDir: configDirFile,
|
|
DataDir: dataDirFile,
|
|
ImageCacheDir: imageCacheDir,
|
|
RuntimeDir: rtDirFile,
|
|
}
|
|
|
|
// make sure all machine dirs are present
|
|
if err := os.MkdirAll(rtDir, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Because this is a mkdirall, we make the image cache dir
|
|
// which is a subdir of datadir (so the datadir is made anyway)
|
|
err = os.MkdirAll(imageCacheDir.GetPath(), 0755)
|
|
|
|
return &dirs, err
|
|
}
|
|
|
|
// 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 define.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
|
|
}
|
|
|
|
// GetSSHIdentityPath returns the path to the expected SSH private key
|
|
func GetSSHIdentityPath(name string) (string, error) {
|
|
datadir, err := GetGlobalDataDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(datadir, name), nil
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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 APIForwardingState int
|
|
|
|
const (
|
|
NoForwarding APIForwardingState = iota
|
|
ClaimUnsupported
|
|
NotInstalled
|
|
MachineLocal
|
|
DockerGlobal
|
|
)
|
|
|
|
// TODO THis should be able to be removed once WSL is refactored for podman5
|
|
type Virtualization struct {
|
|
artifact define.Artifact
|
|
compression compression.ImageCompression
|
|
format define.ImageFormat
|
|
vmKind define.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() define.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 define.VMType) Virtualization {
|
|
return Virtualization{
|
|
artifact,
|
|
compression,
|
|
format,
|
|
vmKind,
|
|
}
|
|
}
|
|
|
|
func dialSocket(socket string, timeout time.Duration) (net.Conn, error) {
|
|
scheme := "unix"
|
|
if strings.Contains(socket, "://") {
|
|
url, err := url.Parse(socket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scheme = url.Scheme
|
|
socket = url.Path
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
var dial func() (net.Conn, error)
|
|
switch scheme {
|
|
default:
|
|
fallthrough
|
|
case "unix":
|
|
dial = func() (net.Conn, error) {
|
|
var dialer net.Dialer
|
|
return dialer.DialContext(ctx, "unix", socket)
|
|
}
|
|
case "npipe":
|
|
dial = func() (net.Conn, error) {
|
|
return DialNamedPipe(ctx, socket)
|
|
}
|
|
}
|
|
|
|
backoff := 500 * time.Millisecond
|
|
for {
|
|
conn, err := dial()
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return conn, err
|
|
}
|
|
|
|
select {
|
|
case <-time.After(backoff):
|
|
backoff *= 2
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func WaitAndPingAPI(sock string) {
|
|
client := http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(context.Context, string, string) (net.Conn, error) {
|
|
con, err := dialSocket(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")
|
|
}
|
|
}
|