mirror of
https://github.com/containers/podman.git
synced 2026-03-28 03:22:18 -04:00
For Podman 6, we still have providers and will continue to have a default provider for each platform. But where a platform has multiple providers, we want users to be able to cross provider boudnaries imposed in Podman 4/5. The key change is to look up virtual machines by name, as before, but to then also iterate all possible providers. As of this PR, init will still only create with the default provider, but a subsequent PR will introdouce an provider override. I also removed the "--all-providers" command line option on `podman machine ls` because it no longer makes sense. And I marked the all provider list test to be skipped. Signed-off-by: Brent Baude <bbaude@redhat.com>
385 lines
9.9 KiB
Go
385 lines
9.9 KiB
Go
package vmconfigs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containers/podman/v6/pkg/errorhandling"
|
|
"github.com/containers/podman/v6/pkg/machine/connection"
|
|
"github.com/containers/podman/v6/pkg/machine/define"
|
|
"github.com/containers/podman/v6/pkg/machine/lock"
|
|
"github.com/containers/podman/v6/pkg/machine/ports"
|
|
"github.com/sirupsen/logrus"
|
|
"go.podman.io/common/pkg/strongunits"
|
|
"go.podman.io/storage/pkg/fileutils"
|
|
"go.podman.io/storage/pkg/ioutils"
|
|
"go.podman.io/storage/pkg/lockfile"
|
|
)
|
|
|
|
/*
|
|
info Display machine host info common
|
|
init Initialize a virtual machine specific
|
|
inspect Inspect an existing machine specific
|
|
list List machines specific
|
|
os Manage a Podman virtual machine's OS common
|
|
rm Remove an existing machine specific
|
|
set Set a virtual machine setting specific
|
|
ssh SSH into an existing machine common
|
|
start Start an existing machine specific
|
|
stop Stop an existing machine specific
|
|
*/
|
|
|
|
var (
|
|
SSHRemoteConnection RemoteConnectionType = "ssh"
|
|
DefaultIgnitionUserName = "core"
|
|
ForwarderBinaryName = "gvproxy"
|
|
)
|
|
|
|
type RemoteConnectionType string
|
|
|
|
// NewMachineConfig creates the initial machine configuration file from cli options.
|
|
func NewMachineConfig(opts define.InitOptions, dirs *define.MachineDirs, sshIdentityPath string, vmtype define.VMType, machineLock *lockfile.LockFile) (*MachineConfig, error) {
|
|
mc := new(MachineConfig)
|
|
mc.Name = opts.Name
|
|
mc.dirs = dirs
|
|
mc.lock = machineLock
|
|
|
|
// Assign Dirs
|
|
cf, err := define.NewMachineFile(filepath.Join(dirs.ConfigDir.GetPath(), fmt.Sprintf("%s.json", opts.Name)), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mc.configPath = cf
|
|
// Given that we are locked now and check again that the config file does not exists,
|
|
// if it does it means the VM was already created and we should error.
|
|
if err := fileutils.Exists(cf.Path); err == nil {
|
|
return nil, &define.ErrVMAlreadyExists{Name: opts.Name}
|
|
}
|
|
|
|
if vmtype != define.QemuVirt && len(opts.USBs) > 0 {
|
|
return nil, fmt.Errorf("USB host passthrough not supported for %s machines", vmtype)
|
|
}
|
|
|
|
usbs, err := define.ParseUSBs(opts.USBs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// System Resources
|
|
mrc := ResourceConfig{
|
|
CPUs: opts.CPUS,
|
|
DiskSize: strongunits.GiB(opts.DiskSize),
|
|
Memory: strongunits.MiB(opts.Memory),
|
|
USBs: usbs,
|
|
}
|
|
mc.Resources = mrc
|
|
|
|
if opts.Swap > 0 {
|
|
mc.Swap = strongunits.MiB(opts.Swap)
|
|
}
|
|
|
|
sshPort, err := ports.AllocateMachinePort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sshConfig := SSHConfig{
|
|
IdentityPath: sshIdentityPath,
|
|
Port: sshPort,
|
|
RemoteUsername: opts.Username,
|
|
}
|
|
|
|
mc.SSH = sshConfig
|
|
mc.Created = time.Now()
|
|
|
|
mc.HostUser = HostUser{UID: getHostUID(), Rootful: opts.Rootful}
|
|
|
|
return mc, nil
|
|
}
|
|
|
|
// Lock creates a lock on the machine for single access
|
|
func (mc *MachineConfig) Lock() {
|
|
mc.lock.Lock()
|
|
}
|
|
|
|
// Unlock removes an existing lock
|
|
func (mc *MachineConfig) Unlock() {
|
|
mc.lock.Unlock()
|
|
}
|
|
|
|
// Refresh reloads the config file from disk
|
|
func (mc *MachineConfig) Refresh() error {
|
|
content, err := os.ReadFile(mc.configPath.GetPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(content, mc)
|
|
}
|
|
|
|
// write is a non-locking way to write the machine configuration file to disk
|
|
func (mc *MachineConfig) Write() error {
|
|
if mc.configPath == nil {
|
|
return fmt.Errorf("no configuration file associated with vm %q", mc.Name)
|
|
}
|
|
b, err := json.Marshal(mc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logrus.Debugf("writing configuration file %q", mc.configPath.Path)
|
|
return ioutils.AtomicWriteFile(mc.configPath.GetPath(), b, define.DefaultFilePerm)
|
|
}
|
|
|
|
func (mc *MachineConfig) SetRootful(rootful bool) error {
|
|
if err := connection.UpdateConnectionIfDefault(rootful, mc.Name, mc.Name+"-root"); err != nil {
|
|
return err
|
|
}
|
|
mc.HostUser.Rootful = rootful
|
|
mc.HostUser.Modified = true
|
|
return nil
|
|
}
|
|
|
|
func (mc *MachineConfig) Remove(machines map[string]bool, saveIgnition, saveImage bool) ([]string, func() error, error) {
|
|
ignitionFile, err := mc.IgnitionFile()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
readySocket, err := mc.ReadySocket()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
gvProxySocket, err := mc.GVProxySocket()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
apiSocket, err := mc.APISocket()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
logPath, err := mc.LogFile()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
rmFiles := []string{
|
|
mc.configPath.GetPath(),
|
|
readySocket.GetPath(),
|
|
gvProxySocket.GetPath(),
|
|
apiSocket.GetPath(),
|
|
logPath.GetPath(),
|
|
}
|
|
if !saveImage {
|
|
mc.ImagePath.GetPath()
|
|
}
|
|
if !saveIgnition {
|
|
ignitionFile.GetPath()
|
|
}
|
|
|
|
mcRemove := func() error {
|
|
var errs []error
|
|
if err := connection.RemoveConnections(machines, mc.Name, mc.Name+"-root"); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
if !saveIgnition {
|
|
if err := ignitionFile.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if !saveImage {
|
|
if err := mc.ImagePath.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if err := readySocket.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if err := gvProxySocket.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if err := apiSocket.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if err := logPath.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
if err := mc.configPath.Delete(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
if err := ports.ReleaseMachinePort(mc.SSH.Port); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
return errorhandling.JoinErrors(errs)
|
|
}
|
|
|
|
return rmFiles, mcRemove, nil
|
|
}
|
|
|
|
// ConfigDir is a simple helper to obtain the machine config dir
|
|
func (mc *MachineConfig) ConfigDir() (*define.VMFile, error) {
|
|
if mc.dirs == nil || mc.dirs.ConfigDir == nil {
|
|
return nil, errors.New("no configuration directory set")
|
|
}
|
|
return mc.dirs.ConfigDir, nil
|
|
}
|
|
|
|
// DataDir is a simple helper function to obtain the machine data dir
|
|
func (mc *MachineConfig) DataDir() (*define.VMFile, error) {
|
|
if mc.dirs == nil || mc.dirs.DataDir == nil {
|
|
return nil, errors.New("no data directory set")
|
|
}
|
|
return mc.dirs.DataDir, nil
|
|
}
|
|
|
|
// RuntimeDir is simple helper function to obtain the runtime dir
|
|
func (mc *MachineConfig) RuntimeDir() (*define.VMFile, error) {
|
|
if mc.dirs == nil || mc.dirs.RuntimeDir == nil {
|
|
return nil, errors.New("no runtime directory set")
|
|
}
|
|
return mc.dirs.RuntimeDir, nil
|
|
}
|
|
|
|
func (mc *MachineConfig) SetDirs(dirs *define.MachineDirs) {
|
|
mc.dirs = dirs
|
|
}
|
|
|
|
func (mc *MachineConfig) IgnitionFile() (*define.VMFile, error) {
|
|
configDir, err := mc.ConfigDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return configDir.AppendToNewVMFile(mc.Name+".ign", nil)
|
|
}
|
|
|
|
func (mc *MachineConfig) ReadySocket() (*define.VMFile, error) {
|
|
rtDir, err := mc.RuntimeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return readySocket(mc.Name, rtDir)
|
|
}
|
|
|
|
func (mc *MachineConfig) GVProxySocket() (*define.VMFile, error) {
|
|
machineRuntimeDir, err := mc.RuntimeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gvProxySocket(mc.Name, machineRuntimeDir)
|
|
}
|
|
|
|
func (mc *MachineConfig) APISocket() (*define.VMFile, error) {
|
|
machineRuntimeDir, err := mc.RuntimeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return apiSocket(mc.Name, machineRuntimeDir)
|
|
}
|
|
|
|
func (mc *MachineConfig) LogFile() (*define.VMFile, error) {
|
|
rtDir, err := mc.RuntimeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rtDir.AppendToNewVMFile(mc.Name+".log", nil)
|
|
}
|
|
|
|
func (mc *MachineConfig) IsFirstBoot() bool {
|
|
return mc.LastUp.IsZero()
|
|
}
|
|
|
|
func (mc *MachineConfig) ConnectionInfo(_ define.VMType) (*define.VMFile, *define.VMFile, error) {
|
|
socket, err := mc.APISocket()
|
|
return socket, getPipe(mc.Name), err
|
|
}
|
|
|
|
// LoadMachineByName returns a machine config based on the vm name and provider
|
|
func LoadMachineByName(name string, dirs *define.MachineDirs) (*MachineConfig, error) {
|
|
fullPath, err := dirs.ConfigDir.AppendToNewVMFile(name+".json", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mc, err := loadMachineFromFQPath(fullPath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, &define.ErrVMDoesNotExist{Name: name}
|
|
}
|
|
return nil, err
|
|
}
|
|
mc.dirs = dirs
|
|
mc.configPath = fullPath
|
|
|
|
// If we find an incompatible configuration, we return a hard
|
|
// error because the user wants to deal directly with this
|
|
// machine
|
|
if mc.Version == 0 {
|
|
return mc, &define.ErrIncompatibleMachineConfig{
|
|
Name: name,
|
|
Path: fullPath.GetPath(),
|
|
}
|
|
}
|
|
return mc, nil
|
|
}
|
|
|
|
// loadMachineFromFQPath stub function for loading a JSON configuration file and returning
|
|
// a machineconfig. this should only be called if you know what you are doing.
|
|
func loadMachineFromFQPath(path *define.VMFile) (*MachineConfig, error) {
|
|
mc := new(MachineConfig)
|
|
b, err := path.Read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = json.Unmarshal(b, mc); err != nil {
|
|
return nil, fmt.Errorf("unable to load machine config file: %q", err)
|
|
}
|
|
lock, err := lock.GetMachineLock(mc.Name, filepath.Dir(path.GetPath()))
|
|
mc.lock = lock
|
|
return mc, err
|
|
}
|
|
|
|
// LoadMachinesInDir returns all the machineconfigs located in given dir
|
|
func LoadMachinesInDir(dirs *define.MachineDirs) (map[string]*MachineConfig, error) {
|
|
mcs := make(map[string]*MachineConfig)
|
|
if err := filepath.WalkDir(dirs.ConfigDir.GetPath(), func(_ string, d fs.DirEntry, _ error) error {
|
|
if strings.HasSuffix(d.Name(), ".json") {
|
|
fullPath, err := dirs.ConfigDir.AppendToNewVMFile(d.Name(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mc, err := loadMachineFromFQPath(fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// if we find an incompatible machine configuration file, we emit and error
|
|
//
|
|
if mc.Version == 0 {
|
|
tmpErr := &define.ErrIncompatibleMachineConfig{
|
|
Name: mc.Name,
|
|
Path: fullPath.GetPath(),
|
|
}
|
|
logrus.Error(tmpErr)
|
|
return nil
|
|
}
|
|
mc.configPath = fullPath
|
|
mc.dirs = dirs
|
|
mcs[mc.Name] = mc
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return mcs, nil
|
|
}
|