diff options
Diffstat (limited to 'pkg/machine')
-rw-r--r-- | pkg/machine/config.go | 9 | ||||
-rw-r--r-- | pkg/machine/config_test.go | 71 | ||||
-rw-r--r-- | pkg/machine/fedora.go | 5 | ||||
-rw-r--r-- | pkg/machine/ignition.go | 5 | ||||
-rw-r--r-- | pkg/machine/pull.go | 2 | ||||
-rw-r--r-- | pkg/machine/qemu/config.go | 124 | ||||
-rw-r--r-- | pkg/machine/qemu/config_test.go | 103 | ||||
-rw-r--r-- | pkg/machine/qemu/machine.go | 81 | ||||
-rw-r--r-- | pkg/machine/wsl/machine.go | 7 |
9 files changed, 367 insertions, 40 deletions
diff --git a/pkg/machine/config.go b/pkg/machine/config.go index b3b105150..7e1561506 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -29,16 +29,16 @@ type InitOptions struct { Username string ReExec bool Rootful bool - // The numberical userid of the user that called machine + // The numerical userid of the user that called machine UID string } type QemuMachineStatus = string const ( - // Running indicates the qemu vm is running + // Running indicates the qemu vm is running. Running QemuMachineStatus = "running" - // Stopped indicates the vm has stopped + // Stopped indicates the vm has stopped. Stopped QemuMachineStatus = "stopped" DefaultMachineName string = "podman-machine-default" ) @@ -128,6 +128,7 @@ type DistributionDownload interface { } 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", @@ -147,7 +148,7 @@ func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url } // GetDataDir returns the filepath where vm images should -// live for podman-machine +// live for podman-machine. func GetDataDir(vmType string) (string, error) { data, err := homedir.GetDataHome() if err != nil { diff --git a/pkg/machine/config_test.go b/pkg/machine/config_test.go new file mode 100644 index 000000000..d9fc5425e --- /dev/null +++ b/pkg/machine/config_test.go @@ -0,0 +1,71 @@ +package machine + +import ( + "net" + "net/url" + "reflect" + "testing" +) + +func TestRemoteConnectionType_MakeSSHURL(t *testing.T) { + var ( + host = "foobar" + path = "/path/to/socket" + rc = "ssh" + username = "core" + ) + type args struct { + host string + path string + port string + userName string + } + tests := []struct { + name string + rc RemoteConnectionType + args args + want url.URL + }{ + { + name: "Good no port", + rc: "ssh", + args: args{ + host: host, + path: path, + port: "", + userName: username, + }, + want: url.URL{ + Scheme: rc, + User: url.User(username), + Host: host, + Path: path, + ForceQuery: false, + }, + }, + { + name: "Good with port", + rc: "ssh", + args: args{ + host: host, + path: path, + port: "222", + userName: username, + }, + want: url.URL{ + Scheme: rc, + User: url.User(username), + Host: net.JoinHostPort(host, "222"), + Path: path, + ForceQuery: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.rc.MakeSSHURL(tt.args.host, tt.args.path, tt.args.port, tt.args.userName); !reflect.DeepEqual(got, tt.want) { //nolint: scopelint + t.Errorf("MakeSSHURL() = %v, want %v", got, tt.want) //nolint: scopelint + } + }) + } +} diff --git a/pkg/machine/fedora.go b/pkg/machine/fedora.go index b26921b52..bed45c6da 100644 --- a/pkg/machine/fedora.go +++ b/pkg/machine/fedora.go @@ -59,7 +59,10 @@ func (f FedoraDownload) Get() *Download { func (f FedoraDownload) HasUsableCache() (bool, error) { info, err := os.Stat(f.LocalPath) if err != nil { - return false, nil + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + return false, err } return info.Size() == f.Size, nil } diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index b2dabb689..fe47437e3 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -6,6 +6,7 @@ package machine import ( "encoding/json" "fmt" + "io/fs" "io/ioutil" "net/url" "os" @@ -507,8 +508,8 @@ func getCerts(certsDir string, isDir bool) []File { ) if isDir { - err := filepath.Walk(certsDir, func(path string, info os.FileInfo, err error) error { - if err == nil && !info.IsDir() { + err := filepath.WalkDir(certsDir, func(path string, d fs.DirEntry, err error) error { + if err == nil && !d.IsDir() { certPath, err := filepath.Rel(certsDir, path) if err != nil { logrus.Warnf("%s", err) diff --git a/pkg/machine/pull.go b/pkg/machine/pull.go index 26abedfcd..7e6f01bad 100644 --- a/pkg/machine/pull.go +++ b/pkg/machine/pull.go @@ -129,7 +129,7 @@ func DownloadVMImage(downloadURL *url2.URL, localImagePath string) error { }() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("error downloading VM image %s: %s", downloadURL, resp.Status) + return fmt.Errorf("downloading VM image %s: %s", downloadURL, resp.Status) } size := resp.ContentLength urlSplit := strings.Split(downloadURL.Path, "/") diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index b39334be0..408b33a33 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -4,12 +4,28 @@ package qemu import ( + "errors" + "os" "time" + + "github.com/sirupsen/logrus" +) + +const ( + // FCOS streams + // Testing FCOS stream + Testing string = "testing" + // Next FCOS stream + Next string = "next" + // Stable FCOS stream + Stable string = "stable" ) type Provider struct{} -type MachineVM struct { +// Deprecated: MachineVMV1 is being deprecated in favor a more flexible and informative +// structure +type MachineVMV1 struct { // CPUs to be assigned to the VM CPUs uint64 // The command line representation of the qemu command @@ -42,6 +58,74 @@ type MachineVM struct { UID int } +type MachineVM struct { + // The command line representation of the qemu command + CmdLine []string + // HostUser contains info about host user + HostUser + // ImageConfig describes the bootable image + ImageConfig + // Mounts is the list of remote filesystems to mount + Mounts []Mount + // Name of VM + Name string + // PidFilePath is the where the PID file lives + PidFilePath MachineFile + // QMPMonitor is the qemu monitor object for sending commands + QMPMonitor Monitor + // ReadySocket tells host when vm is booted + ReadySocket MachineFile + // ResourceConfig is physical attrs of the VM + ResourceConfig + // SSHConfig for accessing the remote vm + SSHConfig +} + +// ImageConfig describes the bootable image for the VM +type ImageConfig struct { + IgnitionFilePath string + // ImageStream is the update stream for the image + ImageStream string + // ImagePath is the fq path to + ImagePath string +} + +// 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 +} + +// 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 +} + +// ResourceConfig describes physical attributes of the machine +type ResourceConfig struct { + // CPUs to be assigned to the VM + CPUs uint64 + // Memory in megabytes assigned to the vm + Memory uint64 + // Disk size in gigabytes assigned to the vm + DiskSize uint64 +} + +type MachineFile struct { + // Path is the fully qualified path to a file + Path string + // Symlink is a shortened version of Path by using + // a symlink + Symlink *string +} + type Mount struct { Type string Tag string @@ -52,7 +136,7 @@ type Mount struct { type Monitor struct { // Address portion of the qmp monitor (/tmp/tmp.sock) - Address string + Address MachineFile // Network portion of the qmp monitor (unix) Network string // Timeout in seconds for qmp monitor transactions @@ -61,6 +145,40 @@ type Monitor struct { var ( // defaultQMPTimeout is the timeout duration for the - // qmp monitor interactions + // qmp monitor interactions. defaultQMPTimeout time.Duration = 2 * time.Second ) + +// GetPath returns the working path for a machinefile. it returns +// the symlink unless one does not exist +func (m *MachineFile) GetPath() string { + if m.Symlink == nil { + return m.Path + } + return *m.Symlink +} + +// Delete removes the machinefile symlink (if it exists) and +// the actual path +func (m *MachineFile) Delete() error { + if m.Symlink != nil { + if err := os.Remove(*m.Symlink); err != nil { + logrus.Errorf("unable to remove symlink %q", *m.Symlink) + } + } + return os.Remove(m.Path) +} + +// NewMachineFile is a constructor for MachineFile +func NewMachineFile(path string, symlink *string) (*MachineFile, error) { + if len(path) < 1 { + return nil, errors.New("invalid machine file path") + } + if symlink != nil && len(*symlink) < 1 { + return nil, errors.New("invalid symlink path") + } + return &MachineFile{ + Path: path, + Symlink: symlink, + }, nil +} diff --git a/pkg/machine/qemu/config_test.go b/pkg/machine/qemu/config_test.go new file mode 100644 index 000000000..e3e7437b5 --- /dev/null +++ b/pkg/machine/qemu/config_test.go @@ -0,0 +1,103 @@ +package qemu + +import ( + "reflect" + "testing" +) + +func TestMachineFile_GetPath(t *testing.T) { + path := "/var/tmp/podman/my.sock" + sym := "/tmp/podman/my.sock" + type fields struct { + Path string + Symlink *string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Original path", + fields: fields{path, nil}, + want: path, + }, + { + name: "Symlink over path", + fields: fields{ + Path: path, + Symlink: &sym, + }, + want: sym, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &MachineFile{ + Path: tt.fields.Path, //nolint: scopelint + Symlink: tt.fields.Symlink, //nolint: scopelint + } + if got := m.GetPath(); got != tt.want { //nolint: scopelint + t.Errorf("GetPath() = %v, want %v", got, tt.want) //nolint: scopelint + } + }) + } +} + +func TestNewMachineFile(t *testing.T) { + p := "/var/tmp/podman/my.sock" + sym := "/tmp/podman/my.sock" + empty := "" + + m := MachineFile{ + Path: p, + Symlink: nil, + } + type args struct { + path string + symlink *string + } + tests := []struct { + name string + args args + want *MachineFile + wantErr bool + }{ + { + name: "Good", + args: args{path: p}, + want: &m, + wantErr: false, + }, + { + name: "Good with Symlink", + args: args{p, &sym}, + want: &MachineFile{p, &sym}, + wantErr: false, + }, + { + name: "Bad path name", + args: args{empty, nil}, + want: nil, + wantErr: true, + }, + { + name: "Bad symlink name", + args: args{p, &empty}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewMachineFile(tt.args.path, tt.args.symlink) //nolint: scopelint + if (err != nil) != tt.wantErr { //nolint: scopelint + t.Errorf("NewMachineFile() error = %v, wantErr %v", err, tt.wantErr) //nolint: scopelint + return + } + if !reflect.DeepEqual(got, tt.want) { //nolint: scopelint + t.Errorf("NewMachineFile() got = %v, want %v", got, tt.want) //nolint: scopelint + } + }) + } +} diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index d30e51215..ac8e7d75c 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -34,7 +34,7 @@ import ( var ( qemuProvider = &Provider{} - // vmtype refers to qemu (vs libvirt, krun, etc) + // vmtype refers to qemu (vs libvirt, krun, etc). vmtype = "qemu" ) @@ -98,7 +98,7 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { return nil, err } - cmd := append([]string{execPath}) + cmd := []string{execPath} // Add memory cmd = append(cmd, []string{"-m", strconv.Itoa(int(vm.Memory))}...) // Add cpus @@ -111,7 +111,7 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { return nil, err } vm.QMPMonitor = monitor - cmd = append(cmd, []string{"-qmp", monitor.Network + ":/" + monitor.Address + ",server=on,wait=off"}...) + cmd = append(cmd, []string{"-qmp", monitor.Network + ":/" + monitor.Address.GetPath() + ",server=on,wait=off"}...) // Add network // Right now the mac address is hardcoded so that the host networking gives it a specific IP address. This is @@ -134,7 +134,8 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { // LoadByName reads a json file that describes a known qemu vm // and returns a vm instance func (p *Provider) LoadVMByName(name string) (machine.VM, error) { - vm := &MachineVM{UID: -1} // posix reserves -1, so use it to signify undefined + vm := &MachineVM{Name: name} + vm.HostUser = HostUser{UID: -1} // posix reserves -1, so use it to signify undefined vmConfigDir, err := machine.GetConfDir(vmtype) if err != nil { return nil, err @@ -176,7 +177,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { v.Rootful = opts.Rootful switch opts.ImagePath { - case "testing", "next", "stable", "": + case Testing, Next, Stable, "": // Get image as usual v.ImageStream = opts.ImagePath dd, err := machine.NewFcosDownloader(vmtype, v.Name, opts.ImagePath) @@ -278,7 +279,9 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { fmt.Println("An ignition path was provided. No SSH connection was added to Podman") } // Write the JSON file - v.writeConfig() + if err := v.writeConfig(); err != nil { + return false, fmt.Errorf("writing JSON file: %w", err) + } // User has provided ignition file so keygen // will be skipped. @@ -315,7 +318,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { resize.Stdout = os.Stdout resize.Stderr = os.Stderr if err := resize.Run(); err != nil { - return false, errors.Errorf("error resizing image: %q", err) + return false, errors.Errorf("resizing image: %q", err) } } // If the user provides an ignition file, we need to @@ -370,7 +373,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { conn net.Conn err error qemuSocketConn net.Conn - wait time.Duration = time.Millisecond * 500 + wait = time.Millisecond * 500 ) if v.isIncompatible() { @@ -428,13 +431,29 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { // Disable graphic window when not in debug mode // Done in start, so we're not suck with the debug level we used on init - if logrus.GetLevel() != logrus.DebugLevel { + if !logrus.IsLevelEnabled(logrus.DebugLevel) { cmd = append(cmd, "-display", "none") } _, err = os.StartProcess(v.CmdLine[0], cmd, attr) if err != nil { - return err + // check if qemu was not found + if !errors.Is(err, os.ErrNotExist) { + return err + } + // lookup qemu again maybe the path was changed, https://github.com/containers/podman/issues/13394 + cfg, err := config.Default() + if err != nil { + return err + } + cmd[0], err = cfg.FindHelperBinary(QemuCommand, true) + if err != nil { + return err + } + _, err = os.StartProcess(cmd[0], cmd, attr) + if err != nil { + return err + } } fmt.Println("Waiting for VM ...") socketPath, err := getRuntimeDir() @@ -558,12 +577,12 @@ func (v *MachineVM) checkStatus(monitor *qmp.SocketMonitor) (machine.QemuMachine func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { var disconnected bool // check if the qmp socket is there. if not, qemu instance is gone - if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) { + if _, err := os.Stat(v.QMPMonitor.Address.GetPath()); os.IsNotExist(err) { // Right now it is NOT an error to stop a stopped machine logrus.Debugf("QMP monitor socket %v does not exist", v.QMPMonitor.Address) return nil } - qmpMonitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout) + qmpMonitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address.GetPath(), v.QMPMonitor.Timeout) if err != nil { return err } @@ -626,7 +645,8 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { } if err := qmpMonitor.Disconnect(); err != nil { - return nil + // FIXME: this error should probably be returned + return nil // nolint: nilerr } disconnected = true @@ -665,20 +685,25 @@ func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) if timeout == 0 { timeout = defaultQMPTimeout } + address, err := NewMachineFile(filepath.Join(rtDir, "qmp+"+name+".sock"), nil) + if err != nil { + return Monitor{}, err + } monitor := Monitor{ Network: network, - Address: filepath.Join(rtDir, "qmp_"+name+".sock"), + Address: *address, Timeout: timeout, } return monitor, nil } +// Remove deletes all the files associated with a machine including ssh keys, the image itself func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { var ( files []string ) - // cannot remove a running vm + // cannot remove a running vm unless --force is used running, err := v.isRunning() if err != nil { return "", nil, err @@ -749,13 +774,14 @@ func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, fun func (v *MachineVM) isRunning() (bool, error) { // Check if qmp socket path exists - if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) { + if _, err := os.Stat(v.QMPMonitor.Address.GetPath()); os.IsNotExist(err) { return false, nil } // Check if we can dial it - monitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout) + monitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address.GetPath(), v.QMPMonitor.Timeout) if err != nil { - return false, nil + // FIXME: this error should probably be returned + return false, nil // nolint: nilerr } if err := monitor.Connect(); err != nil { return false, err @@ -778,7 +804,7 @@ func (v *MachineVM) isRunning() (bool, error) { func (v *MachineVM) isListening() bool { // Check if we can dial it - conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", "localhost", v.Port), 10*time.Millisecond) + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", v.Port), 10*time.Millisecond) if err != nil { return false } @@ -875,10 +901,10 @@ func GetVMInfos() ([]*machine.ListResponse, error) { var listed []*machine.ListResponse - if err = filepath.Walk(vmConfigDir, func(path string, info os.FileInfo, err error) error { + if err = filepath.WalkDir(vmConfigDir, func(path string, d fs.DirEntry, err error) error { vm := new(MachineVM) - if strings.HasSuffix(info.Name(), ".json") { - fullPath := filepath.Join(vmConfigDir, info.Name()) + if strings.HasSuffix(d.Name(), ".json") { + fullPath := filepath.Join(vmConfigDir, d.Name()) b, err := ioutil.ReadFile(fullPath) if err != nil { return err @@ -1058,7 +1084,7 @@ func (v *MachineVM) isIncompatible() bool { func (v *MachineVM) getForwardSocketPath() (string, error) { path, err := machine.GetDataDir(v.Name) if err != nil { - logrus.Errorf("Error resolving data dir: %s", err.Error()) + logrus.Errorf("Resolving data dir: %s", err.Error()) return "", nil } return filepath.Join(path, "podman.sock"), nil @@ -1097,10 +1123,13 @@ func waitAndPingAPI(sock string) { Transport: &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { con, err := net.DialTimeout("unix", sock, apiUpTimeout) - if err == nil { - con.SetDeadline(time.Now().Add(apiUpTimeout)) + if err != nil { + return nil, err + } + if err := con.SetDeadline(time.Now().Add(apiUpTimeout)); err != nil { + return nil, err } - return con, err + return con, nil }, }, } diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 5b0c757f0..5128fa313 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "io/ioutil" "net/url" "os" @@ -1175,10 +1176,10 @@ func GetVMInfos() ([]*machine.ListResponse, error) { var listed []*machine.ListResponse - if err = filepath.Walk(vmConfigDir, func(path string, info os.FileInfo, err error) error { + if err = filepath.WalkDir(vmConfigDir, func(path string, d fs.DirEntry, err error) error { vm := new(MachineVM) - if strings.HasSuffix(info.Name(), ".json") { - fullPath := filepath.Join(vmConfigDir, info.Name()) + if strings.HasSuffix(d.Name(), ".json") { + fullPath := filepath.Join(vmConfigDir, d.Name()) b, err := ioutil.ReadFile(fullPath) if err != nil { return err |