Files
Jake Correnti d68240b150 Update machine files rm order and add QEMU rm
Changes the order in which the machine-specific files are removed in
`Remove()`. Removes the system connections first, then removes the
`configPath` last. `configPath` is removed last, because in the case of
an error with any of the previous files, the removal can be attempted
again since the machine still "exists".

Made the errors in `Remove` hard errors instead of soft errors.

Added the implementation for the QEMU-specific file removal.

[NO NEW TESTS NEEDED]

Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
2024-02-22 08:31:55 -05:00

412 lines
10 KiB
Go

package vmconfigs
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
define2 "github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/podman/v5/pkg/machine/connection"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/lock"
"github.com/containers/podman/v5/utils"
"github.com/sirupsen/logrus"
)
/*
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) (*MachineConfig, error) {
mc := new(MachineConfig)
mc.Name = opts.Name
mc.dirs = dirs
machineLock, err := lock.GetMachineLock(opts.Name, dirs.ConfigDir.GetPath())
if err != nil {
return nil, err
}
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
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: opts.DiskSize,
Memory: opts.Memory,
USBs: usbs,
}
mc.Resources = mrc
// TODO WSL had a locking port mechanism, we should consider this.
sshPort, err := utils.GetRandomPort()
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()
}
// Write is a locking way to the machine configuration file
func (mc *MachineConfig) Write() error {
mc.Lock()
defer mc.Unlock()
return mc.write()
}
// 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 os.WriteFile(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) removeSystemConnection() error { //nolint:unused
return define2.ErrNotImplemented
}
// updateLastBoot writes the current time to the machine configuration file. it is
// an non-locking method and assumes it is being called locked
func (mc *MachineConfig) updateLastBoot() error { //nolint:unused
mc.LastUp = time.Now()
return mc.Write()
}
func (mc *MachineConfig) Remove(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
}
logPath, err := mc.LogFile()
if err != nil {
return nil, nil, err
}
rmFiles := []string{
mc.configPath.GetPath(),
readySocket.GetPath(),
logPath.GetPath(),
}
if !saveImage {
mc.ImagePath.GetPath()
}
if !saveIgnition {
ignitionFile.GetPath()
}
mcRemove := func() error {
var errs []error
if err := connection.RemoveConnections(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 := logPath.Delete(); err != nil {
errs = append(errs, err)
}
if err := mc.configPath.Delete(); 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) LogFile() (*define.VMFile, error) {
rtDir, err := mc.RuntimeDir()
if err != nil {
return nil, err
}
return rtDir.AppendToNewVMFile(mc.Name+".log", nil)
}
func (mc *MachineConfig) Kind() (define.VMType, error) {
// Not super in love with this approach
if mc.QEMUHypervisor != nil {
return define.QemuVirt, nil
}
if mc.AppleHypervisor != nil {
return define.AppleHvVirt, nil
}
if mc.HyperVHypervisor != nil {
return define.HyperVVirt, nil
}
if mc.WSLHypervisor != nil {
return define.WSLVirt, nil
}
return define.UnknownVirt, nil
}
func (mc *MachineConfig) IsFirstBoot() (bool, error) {
never, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z")
if err != nil {
return false, err
}
return mc.LastUp == never, nil
}
func (mc *MachineConfig) ConnectionInfo(vmtype define.VMType) (*define.VMFile, *define.VMFile, error) {
var (
socket *define.VMFile
pipe *define.VMFile
)
if vmtype == define.HyperVVirt || vmtype == define.WSLVirt {
pipeName := mc.Name
if !strings.HasPrefix(pipeName, "podman") {
pipeName = "podman-" + pipeName
}
pipe = &define.VMFile{Path: `\\.\pipe\` + pipeName}
}
if vmtype == define.WSLVirt {
return nil, pipe, nil
}
sockName := "podman.sock"
dataDir, err := mc.DataDir()
if err != nil {
logrus.Errorf("Resolving data dir: %s", err.Error())
return nil, nil, err
}
socket, err = define.NewMachineFile(filepath.Join(dataDir.Path, sockName), &sockName)
return socket, pipe, 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(path string, d fs.DirEntry, err 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
}