From b5f54a9b23e8d9418700494da9aa78d8db354c43 Mon Sep 17 00:00:00 2001 From: baude Date: Mon, 15 Mar 2021 14:52:43 -0500 Subject: introduce podman machine podman machine allows podman to create, manage, and interact with a vm running some form of linux (default is fcos). podman is then configured to be able to interact with the vm automatically. while this is usable on linux, the real push is to get this working on both current apple architectures in macos. Ashley Cui contributed to this PR and was a great help. [NO TESTS NEEDED] Signed-off-by: baude --- pkg/machine/config.go | 132 +++++++++++++++++ pkg/machine/connection.go | 50 +++++++ pkg/machine/fcos.go | 162 ++++++++++++++++++++ pkg/machine/fcos_amd64.go | 68 +++++++++ pkg/machine/fcos_arm64.go | 169 +++++++++++++++++++++ pkg/machine/ignition.go | 151 +++++++++++++++++++ pkg/machine/ignition_schema.go | 251 +++++++++++++++++++++++++++++++ pkg/machine/keys.go | 25 ++++ pkg/machine/libvirt/config.go | 4 + pkg/machine/libvirt/machine.go | 15 ++ pkg/machine/pull.go | 97 ++++++++++++ pkg/machine/qemu/config.go | 43 ++++++ pkg/machine/qemu/machine.go | 304 ++++++++++++++++++++++++++++++++++++++ pkg/machine/qemu/options_amd64.go | 26 ++++ pkg/machine/qemu/options_arm64.go | 40 +++++ 15 files changed, 1537 insertions(+) create mode 100644 pkg/machine/config.go create mode 100644 pkg/machine/connection.go create mode 100644 pkg/machine/fcos.go create mode 100644 pkg/machine/fcos_amd64.go create mode 100644 pkg/machine/fcos_arm64.go create mode 100644 pkg/machine/ignition.go create mode 100644 pkg/machine/ignition_schema.go create mode 100644 pkg/machine/keys.go create mode 100644 pkg/machine/libvirt/config.go create mode 100644 pkg/machine/libvirt/machine.go create mode 100644 pkg/machine/pull.go create mode 100644 pkg/machine/qemu/config.go create mode 100644 pkg/machine/qemu/machine.go create mode 100644 pkg/machine/qemu/options_amd64.go create mode 100644 pkg/machine/qemu/options_arm64.go (limited to 'pkg/machine') diff --git a/pkg/machine/config.go b/pkg/machine/config.go new file mode 100644 index 000000000..5e90dae51 --- /dev/null +++ b/pkg/machine/config.go @@ -0,0 +1,132 @@ +package machine + +import ( + "net" + "net/url" + "os" + "path/filepath" + + "github.com/containers/storage/pkg/homedir" +) + +type CreateOptions struct { + Name string + CPUS uint64 + Memory uint64 + IgnitionPath string + ImagePath string + Username string + URI url.URL + IsDefault bool + //KernelPath string + //Devices []VMDevices +} + +type RemoteConnectionType string + +var ( + SSHRemoteConnection RemoteConnectionType = "ssh" + DefaultIgnitionUserName = "core" +) + +type Download struct { + Arch string + Artifact string + CompressionType string + Format string + ImageName string `json:"image_name"` + LocalPath string + LocalUncompressedFile string + Sha256sum string + URL *url.URL + VMName string +} + +type SSHOptions struct{} +type StartOptions struct{} + +type StopOptions struct{} + +type DestroyOptions struct { + Force bool + SaveKeys bool + SaveImage bool + SaveIgnition bool +} + +type VM interface { + Create(opts CreateOptions) error + Destroy(name string, opts DestroyOptions) (string, func() error, error) + SSH(name string, opts SSHOptions) error + Start(name string, opts StartOptions) error + Stop(name string, opts StopOptions) error +} + +type DistributionDownload interface { + DownloadImage() error + Get() *Download +} + +// TODO is this even needed? +type TestVM struct{} + +func (vm *TestVM) Create(opts CreateOptions) error { + return nil +} + +func (vm *TestVM) Start(name string, opts StartOptions) error { + return nil +} +func (vm *TestVM) Stop(name string, opts StopOptions) error { + return nil +} + +func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL { + userInfo := url.User(userName) + uri := url.URL{ + Scheme: "ssh", + Opaque: "", + User: userInfo, + Host: host, + Path: path, + RawPath: "", + ForceQuery: false, + RawQuery: "", + Fragment: "", + RawFragment: "", + } + if len(port) > 0 { + uri.Host = net.JoinHostPort(uri.Hostname(), port) + } + return uri +} + +// GetDataDir returns the filepath where vm images should +// live for podman-machine +func GetDataDir(vmType string) (string, error) { + data, err := homedir.GetDataHome() + if err != nil { + return "", err + } + dataDir := filepath.Join(data, "containers", "podman", "machine", vmType) + if _, err := os.Stat(dataDir); !os.IsNotExist(err) { + return dataDir, nil + } + mkdirErr := os.MkdirAll(dataDir, 0755) + return dataDir, mkdirErr +} + +// GetConfigDir returns the filepath to where configuration +// files for podman-machine should live +func GetConfDir(vmType string) (string, error) { + conf, err := homedir.GetConfigHome() + if err != nil { + return "", err + } + confDir := filepath.Join(conf, "containers", "podman", "machine", vmType) + if _, err := os.Stat(confDir); !os.IsNotExist(err) { + return confDir, nil + } + mkdirErr := os.MkdirAll(confDir, 0755) + return confDir, mkdirErr +} diff --git a/pkg/machine/connection.go b/pkg/machine/connection.go new file mode 100644 index 000000000..e3985d8ac --- /dev/null +++ b/pkg/machine/connection.go @@ -0,0 +1,50 @@ +package machine + +import ( + "fmt" + + "github.com/containers/common/pkg/config" + "github.com/pkg/errors" +) + +func AddConnection(uri fmt.Stringer, name, identity string, isDefault bool) error { + if len(identity) < 1 { + return errors.New("identity must be defined") + } + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + if _, ok := cfg.Engine.ServiceDestinations[name]; ok { + return errors.New("cannot overwrite connection") + } + if isDefault { + cfg.Engine.ActiveService = name + } + dst := config.Destination{ + URI: uri.String(), + } + dst.Identity = identity + if cfg.Engine.ServiceDestinations == nil { + cfg.Engine.ServiceDestinations = map[string]config.Destination{ + name: dst, + } + cfg.Engine.ActiveService = name + } else { + cfg.Engine.ServiceDestinations[name] = dst + } + return cfg.Write() +} + +func RemoveConnection(name string) error { + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + if _, ok := cfg.Engine.ServiceDestinations[name]; ok { + delete(cfg.Engine.ServiceDestinations, name) + } else { + return errors.Errorf("unable to find connection named %q", name) + } + return cfg.Write() +} diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go new file mode 100644 index 000000000..8bbad458f --- /dev/null +++ b/pkg/machine/fcos.go @@ -0,0 +1,162 @@ +package machine + +import ( + "crypto/sha256" + "io" + "io/ioutil" + url2 "net/url" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/containers/storage/pkg/archive" + + "github.com/sirupsen/logrus" + + digest "github.com/opencontainers/go-digest" +) + +// These should eventually be moved into machine/qemu as +// they are specific to running qemu +var ( + artifact string = "qemu" + Format string = "qcow2.xz" +) + +type FcosDownload struct { + Download +} + +func NewFcosDownloader(vmType, vmName string) (DistributionDownload, error) { + info, err := getFCOSDownload() + if err != nil { + return nil, err + } + urlSplit := strings.Split(info.Location, "/") + imageName := urlSplit[len(urlSplit)-1] + url, err := url2.Parse(info.Location) + if err != nil { + return nil, err + } + + dataDir, err := GetDataDir(vmType) + if err != nil { + return nil, err + } + + fcd := FcosDownload{ + Download: Download{ + Arch: getFcosArch(), + Artifact: artifact, + Format: Format, + ImageName: imageName, + LocalPath: filepath.Join(dataDir, imageName), + Sha256sum: info.Sha256Sum, + URL: url, + VMName: vmName, + }, + } + fcd.Download.LocalUncompressedFile = fcd.getLocalUncompressedName() + return fcd, nil +} + +func (f FcosDownload) getLocalUncompressedName() string { + uncompressedFilename := filepath.Join(filepath.Dir(f.LocalPath), f.VMName+"_"+f.ImageName) + return strings.TrimSuffix(uncompressedFilename, ".xz") +} + +func (f FcosDownload) DownloadImage() error { + // check if the latest image is already present + ok, err := UpdateAvailable(&f.Download) + if err != nil { + return err + } + if !ok { + if err := DownloadVMImage(f.URL, f.LocalPath); err != nil { + return err + } + } + uncompressedFileWriter, err := os.OpenFile(f.getLocalUncompressedName(), os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return err + } + sourceFile, err := ioutil.ReadFile(f.LocalPath) + if err != nil { + return err + } + compressionType := archive.DetectCompression(sourceFile) + f.CompressionType = compressionType.Extension() + + switch f.CompressionType { + case "tar.xz": + return decompressXZ(f.LocalPath, uncompressedFileWriter) + default: + // File seems to be uncompressed, make a copy + if err := copyFile(f.LocalPath, uncompressedFileWriter); err != nil { + return err + } + } + return nil +} + +func copyFile(src string, dest *os.File) error { + source, err := os.Open(src) + if err != nil { + return err + } + defer func() { + if err := source.Close(); err != nil { + logrus.Error(err) + } + }() + _, err = io.Copy(dest, source) + return err +} + +func (f FcosDownload) Get() *Download { + return &f.Download +} + +type fcosDownloadInfo struct { + CompressionType string + Location string + Release string + Sha256Sum string +} + +func UpdateAvailable(d *Download) (bool, error) { + // check the sha of the local image if it exists + // get the sha of the remote image + // == dont bother to pull + files, err := ioutil.ReadDir(filepath.Dir(d.LocalPath)) + if err != nil { + return false, err + } + for _, file := range files { + if filepath.Base(d.LocalPath) == file.Name() { + b, err := ioutil.ReadFile(d.LocalPath) + if err != nil { + return false, err + } + s := sha256.Sum256(b) + sum := digest.NewDigestFromBytes(digest.SHA256, s[:]) + if sum.Encoded() == d.Sha256sum { + return true, nil + } + } + } + return false, nil +} + +func getFcosArch() string { + var arch string + // TODO fill in more architectures + switch runtime.GOARCH { + case "arm64": + arch = "aarch64" + default: + arch = "x86_64" + } + return arch +} diff --git a/pkg/machine/fcos_amd64.go b/pkg/machine/fcos_amd64.go new file mode 100644 index 000000000..36676405a --- /dev/null +++ b/pkg/machine/fcos_amd64.go @@ -0,0 +1,68 @@ +package machine + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/coreos/stream-metadata-go/fedoracoreos" + "github.com/coreos/stream-metadata-go/stream" + "github.com/sirupsen/logrus" +) + +// This should get Exported and stay put as it will apply to all fcos downloads +// getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version +func getFCOSDownload() (*fcosDownloadInfo, error) { + var ( + fcosstable stream.Stream + ) + streamurl := fedoracoreos.GetStreamURL(fedoracoreos.StreamNext) + resp, err := http.Get(streamurl.String()) + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Error(err) + } + }() + + if err := json.Unmarshal(body, &fcosstable); err != nil { + return nil, err + } + arch, ok := fcosstable.Architectures[getFcosArch()] + if !ok { + return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream") + } + artifacts := arch.Artifacts + if artifacts == nil { + return nil, fmt.Errorf("unable to pull VM image: no artifact in stream") + } + qemu, ok := artifacts[artifact] + if !ok { + return nil, fmt.Errorf("unable to pull VM image: no qemu artifact in stream") + } + formats := qemu.Formats + if formats == nil { + return nil, fmt.Errorf("unable to pull VM image: no formats in stream") + } + qcow, ok := formats[Format] + if !ok { + return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream") + } + disk := qcow.Disk + if disk == nil { + return nil, fmt.Errorf("unable to pull VM image: no disk in stream") + } + return &fcosDownloadInfo{ + Location: disk.Location, + Release: qemu.Release, + Sha256Sum: disk.Sha256, + CompressionType: "xz", + }, nil +} diff --git a/pkg/machine/fcos_arm64.go b/pkg/machine/fcos_arm64.go new file mode 100644 index 000000000..ab50ca874 --- /dev/null +++ b/pkg/machine/fcos_arm64.go @@ -0,0 +1,169 @@ +package machine + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/sirupsen/logrus" +) + +const aarchBaseURL = "https://fedorapeople.org/groups/fcos-images/builds/latest/aarch64/" + +// Total hack until automation is possible. +// We need a proper json file at least to automate +func getFCOSDownload() (*fcosDownloadInfo, error) { + + meta := Build{} + fmt.Println(aarchBaseURL + "meta.json") + resp, err := http.Get(aarchBaseURL + "meta.json") + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Error(err) + } + }() + if err := json.Unmarshal(body, &meta); err != nil { + return nil, err + } + return &fcosDownloadInfo{ + Location: "https://fedorapeople.org/groups/fcos-images/builds/latest/aarch64/fedora-coreos-33.20210310.dev.0-qemu.aarch64.qcow2", + Release: "", + Sha256Sum: meta.BuildArtifacts.Qemu.Sha256, + }, nil +} + +/* + All of this can be nuked when fcos upstream generates a proper meta data file for aarch. +*/ +type AliyunImage struct { + ImageID string `json:"id"` + Region string `json:"name"` +} + +type Amis struct { + Hvm string `json:"hvm"` + Region string `json:"name"` + Snapshot string `json:"snapshot"` +} + +type Artifact struct { + Path string `json:"path"` + Sha256 string `json:"sha256"` + SizeInBytes float64 `json:"size,omitempty"` + UncompressedSha256 string `json:"uncompressed-sha256,omitempty"` + UncompressedSize int `json:"uncompressed-size,omitempty"` +} + +type Build struct { + AlibabaAliyunUploads []AliyunImage `json:"aliyun,omitempty"` + Amis []Amis `json:"amis,omitempty"` + Architecture string `json:"coreos-assembler.basearch,omitempty"` + Azure *Cloudartifact `json:"azure,omitempty"` + BuildArtifacts *BuildArtifacts `json:"images,omitempty"` + BuildID string `json:"buildid"` + BuildRef string `json:"ref,omitempty"` + BuildSummary string `json:"summary"` + BuildTimeStamp string `json:"coreos-assembler.build-timestamp,omitempty"` + BuildURL string `json:"build-url,omitempty"` + ConfigGitRev string `json:"coreos-assembler.config-gitrev,omitempty"` + ContainerConfigGit *Git `json:"coreos-assembler.container-config-git,omitempty"` + CoreOsSource string `json:"coreos-assembler.code-source,omitempty"` + CosaContainerImageGit *Git `json:"coreos-assembler.container-image-git,omitempty"` + CosaDelayedMetaMerge bool `json:"coreos-assembler.delayed-meta-merge,omitempty"` + CosaImageChecksum string `json:"coreos-assembler.image-config-checksum,omitempty"` + CosaImageVersion int `json:"coreos-assembler.image-genver,omitempty"` + Extensions *Extensions `json:"extensions,omitempty"` + FedoraCoreOsParentCommit string `json:"fedora-coreos.parent-commit,omitempty"` + FedoraCoreOsParentVersion string `json:"fedora-coreos.parent-version,omitempty"` + Gcp *Gcp `json:"gcp,omitempty"` + GitDirty string `json:"coreos-assembler.config-dirty,omitempty"` + ImageInputChecksum string `json:"coreos-assembler.image-input-checksum,omitempty"` + InputHasOfTheRpmOstree string `json:"rpm-ostree-inputhash"` + MetaStamp float64 `json:"coreos-assembler.meta-stamp,omitempty"` + Name string `json:"name"` + Oscontainer *Image `json:"oscontainer,omitempty"` + OstreeCommit string `json:"ostree-commit"` + OstreeContentBytesWritten int `json:"ostree-content-bytes-written,omitempty"` + OstreeContentChecksum string `json:"ostree-content-checksum"` + OstreeNCacheHits int `json:"ostree-n-cache-hits,omitempty"` + OstreeNContentTotal int `json:"ostree-n-content-total,omitempty"` + OstreeNContentWritten int `json:"ostree-n-content-written,omitempty"` + OstreeNMetadataTotal int `json:"ostree-n-metadata-total,omitempty"` + OstreeNMetadataWritten int `json:"ostree-n-metadata-written,omitempty"` + OstreeTimestamp string `json:"ostree-timestamp"` + OstreeVersion string `json:"ostree-version"` + OverridesActive bool `json:"coreos-assembler.overrides-active,omitempty"` + PkgdiffAgainstParent PackageSetDifferences `json:"parent-pkgdiff,omitempty"` + PkgdiffBetweenBuilds PackageSetDifferences `json:"pkgdiff,omitempty"` + ReleasePayload *Image `json:"release-payload,omitempty"` +} + +type BuildArtifacts struct { + Aliyun *Artifact `json:"aliyun,omitempty"` + Aws *Artifact `json:"aws,omitempty"` + Azure *Artifact `json:"azure,omitempty"` + AzureStack *Artifact `json:"azurestack,omitempty"` + Dasd *Artifact `json:"dasd,omitempty"` + DigitalOcean *Artifact `json:"digitalocean,omitempty"` + Exoscale *Artifact `json:"exoscale,omitempty"` + Gcp *Artifact `json:"gcp,omitempty"` + IbmCloud *Artifact `json:"ibmcloud,omitempty"` + Initramfs *Artifact `json:"initramfs,omitempty"` + Iso *Artifact `json:"iso,omitempty"` + Kernel *Artifact `json:"kernel,omitempty"` + LiveInitramfs *Artifact `json:"live-initramfs,omitempty"` + LiveIso *Artifact `json:"live-iso,omitempty"` + LiveKernel *Artifact `json:"live-kernel,omitempty"` + LiveRootfs *Artifact `json:"live-rootfs,omitempty"` + Metal *Artifact `json:"metal,omitempty"` + Metal4KNative *Artifact `json:"metal4k,omitempty"` + OpenStack *Artifact `json:"openstack,omitempty"` + Ostree Artifact `json:"ostree"` + Qemu *Artifact `json:"qemu,omitempty"` + Vmware *Artifact `json:"vmware,omitempty"` + Vultr *Artifact `json:"vultr,omitempty"` +} + +type Cloudartifact struct { + Image string `json:"image"` + URL string `json:"url"` +} + +type Extensions struct { + Manifest map[string]interface{} `json:"manifest"` + Path string `json:"path"` + RpmOstreeState string `json:"rpm-ostree-state"` + Sha256 string `json:"sha256"` +} + +type Gcp struct { + ImageFamily string `json:"family,omitempty"` + ImageName string `json:"image"` + ImageProject string `json:"project,omitempty"` + URL string `json:"url"` +} + +type Git struct { + Branch string `json:"branch,omitempty"` + Commit string `json:"commit"` + Dirty string `json:"dirty,omitempty"` + Origin string `json:"origin"` +} + +type Image struct { + Comment string `json:"comment,omitempty"` + Digest string `json:"digest"` + Image string `json:"image"` +} + +type Items interface{} + +type PackageSetDifferences []Items diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go new file mode 100644 index 000000000..ff79d5afb --- /dev/null +++ b/pkg/machine/ignition.go @@ -0,0 +1,151 @@ +package machine + +import ( + "encoding/json" + "io/ioutil" +) + +/* + If this file gets too nuts, we can perhaps use existing go code + to create ignition files. At this point, the file is so simple + that I chose to use structs and not import any code as I was + concerned (unsubstantiated) about too much bloat coming in. + + https://github.com/openshift/machine-config-operator/blob/master/pkg/server/server.go +*/ + +// Convenience function to convert int to ptr +func intToPtr(i int) *int { + return &i +} + +// Convenience function to convert string to ptr +func strToPtr(s string) *string { + return &s +} + +// Convenience function to convert bool to ptr +func boolToPtr(b bool) *bool { + return &b +} + +func getNodeUsr(usrName string) NodeUser { + return NodeUser{Name: &usrName} +} + +func getNodeGrp(grpName string) NodeGroup { + return NodeGroup{Name: &grpName} +} + +// NewIgnitionFile +func NewIgnitionFile(name, key, writePath string) error { + if len(name) < 1 { + name = DefaultIgnitionUserName + } + ignVersion := Ignition{ + Version: "3.2.0", + } + + ignPassword := Passwd{ + Users: []PasswdUser{{ + Name: name, + SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(key)}, + }}, + } + + ignStorage := Storage{ + Directories: getDirs(name), + Files: getFiles(name), + Links: getLinks(name), + } + ignSystemd := Systemd{ + Units: []Unit{ + { + Enabled: boolToPtr(true), + Name: "podman.socket", + }}} + + ignConfig := Config{ + Ignition: ignVersion, + Passwd: ignPassword, + Storage: ignStorage, + Systemd: ignSystemd, + } + b, err := json.Marshal(ignConfig) + if err != nil { + return err + } + return ioutil.WriteFile(writePath, b, 0644) +} + +func getDirs(usrName string) []Directory { + // Ignition has a bug/feature? where if you make a series of dirs + // in one swoop, then the leading dirs are creates as root. + newDirs := []string{ + "/home/" + usrName + "/.config", + "/home/" + usrName + "/.config/systemd", + "/home/" + usrName + "/.config/systemd/user", + "/home/" + usrName + "/.config/systemd/user/default.target.wants", + } + var ( + dirs = make([]Directory, len(newDirs)) + ) + for i, d := range newDirs { + newDir := Directory{ + Node: Node{ + Group: getNodeGrp(usrName), + Path: d, + User: getNodeUsr(usrName), + }, + DirectoryEmbedded1: DirectoryEmbedded1{Mode: intToPtr(493)}, + } + dirs[i] = newDir + } + return dirs +} + +func getFiles(usrName string) []File { + var ( + files []File + ) + // Add a fake systemd service to get the user socket rolling + files = append(files, File{ + Node: Node{ + Group: getNodeGrp(usrName), + Path: "/home/" + usrName + "/.config/systemd/user/linger-example.service", + User: getNodeUsr(usrName), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: strToPtr("data:,%5BUnit%5D%0ADescription%3DA%20systemd%20user%20unit%20demo%0AAfter%3Dnetwork-online.target%0AWants%3Dnetwork-online.target%20podman.socket%0A%5BService%5D%0AExecStart%3D%2Fusr%2Fbin%2Fsleep%20infinity%0A"), + }, + Mode: intToPtr(484), + }, + }) + + // Add a file into linger + files = append(files, File{ + Node: Node{ + Group: getNodeGrp(usrName), + Path: "/var/lib/systemd/linger/core", + User: getNodeUsr(usrName), + }, + FileEmbedded1: FileEmbedded1{Mode: intToPtr(420)}, + }) + return files +} + +func getLinks(usrName string) []Link { + return []Link{{ + Node: Node{ + Group: getNodeGrp(usrName), + Path: "/home/" + usrName + "/.config/systemd/user/default.target.wants/linger-example.service", + User: getNodeUsr(usrName), + }, + LinkEmbedded1: LinkEmbedded1{ + Hard: boolToPtr(false), + Target: "/home/" + usrName + "/.config/systemd/user/linger-example.service", + }, + }} +} diff --git a/pkg/machine/ignition_schema.go b/pkg/machine/ignition_schema.go new file mode 100644 index 000000000..9dbd90ba4 --- /dev/null +++ b/pkg/machine/ignition_schema.go @@ -0,0 +1,251 @@ +package machine + +/* + This file was taken from https://github.com/coreos/ignition/blob/master/config/v3_2/types/schema.go in an effort to + use more of the core-os structs but not fully commit to bringing their api in. + + // generated by "schematyper --package=types config/v3_2/schema/ignition.json -o config/v3_2/types/ignition_schema.go --root-type=Config" -- DO NOT EDIT +*/ + +type Clevis struct { + Custom *Custom `json:"custom,omitempty"` + Tang []Tang `json:"tang,omitempty"` + Threshold *int `json:"threshold,omitempty"` + Tpm2 *bool `json:"tpm2,omitempty"` +} + +type Config struct { + Ignition Ignition `json:"ignition"` + Passwd Passwd `json:"passwd,omitempty"` + Storage Storage `json:"storage,omitempty"` + Systemd Systemd `json:"systemd,omitempty"` +} + +type Custom struct { + Config string `json:"config"` + NeedsNetwork *bool `json:"needsNetwork,omitempty"` + Pin string `json:"pin"` +} + +type Device string + +type Directory struct { + Node + DirectoryEmbedded1 +} + +type DirectoryEmbedded1 struct { + Mode *int `json:"mode,omitempty"` +} + +type Disk struct { + Device string `json:"device"` + Partitions []Partition `json:"partitions,omitempty"` + WipeTable *bool `json:"wipeTable,omitempty"` +} + +type Dropin struct { + Contents *string `json:"contents,omitempty"` + Name string `json:"name"` +} + +type File struct { + Node + FileEmbedded1 +} + +type FileEmbedded1 struct { + Append []Resource `json:"append,omitempty"` + Contents Resource `json:"contents,omitempty"` + Mode *int `json:"mode,omitempty"` +} + +type Filesystem struct { + Device string `json:"device"` + Format *string `json:"format,omitempty"` + Label *string `json:"label,omitempty"` + MountOptions []MountOption `json:"mountOptions,omitempty"` + Options []FilesystemOption `json:"options,omitempty"` + Path *string `json:"path,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeFilesystem *bool `json:"wipeFilesystem,omitempty"` +} + +type FilesystemOption string + +type Group string + +type HTTPHeader struct { + Name string `json:"name"` + Value *string `json:"value,omitempty"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `json:"config,omitempty"` + Proxy Proxy `json:"proxy,omitempty"` + Security Security `json:"security,omitempty"` + Timeouts Timeouts `json:"timeouts,omitempty"` + Version string `json:"version,omitempty"` +} + +type IgnitionConfig struct { + Merge []Resource `json:"merge,omitempty"` + Replace Resource `json:"replace,omitempty"` +} + +type Link struct { + Node + LinkEmbedded1 +} + +type LinkEmbedded1 struct { + Hard *bool `json:"hard,omitempty"` + Target string `json:"target"` +} + +type Luks struct { + Clevis *Clevis `json:"clevis,omitempty"` + Device *string `json:"device,omitempty"` + KeyFile Resource `json:"keyFile,omitempty"` + Label *string `json:"label,omitempty"` + Name string `json:"name"` + Options []LuksOption `json:"options,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeVolume *bool `json:"wipeVolume,omitempty"` +} + +type LuksOption string + +type MountOption string + +type NoProxyItem string + +type Node struct { + Group NodeGroup `json:"group,omitempty"` + Overwrite *bool `json:"overwrite,omitempty"` + Path string `json:"path"` + User NodeUser `json:"user,omitempty"` +} + +type NodeGroup struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +type NodeUser struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +type Partition struct { + GUID *string `json:"guid,omitempty"` + Label *string `json:"label,omitempty"` + Number int `json:"number,omitempty"` + Resize *bool `json:"resize,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + SizeMiB *int `json:"sizeMiB,omitempty"` + StartMiB *int `json:"startMiB,omitempty"` + TypeGUID *string `json:"typeGuid,omitempty"` + WipePartitionEntry *bool `json:"wipePartitionEntry,omitempty"` +} + +type Passwd struct { + Groups []PasswdGroup `json:"groups,omitempty"` + Users []PasswdUser `json:"users,omitempty"` +} + +type PasswdGroup struct { + Gid *int `json:"gid,omitempty"` + Name string `json:"name"` + PasswordHash *string `json:"passwordHash,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + System *bool `json:"system,omitempty"` +} + +type PasswdUser struct { + Gecos *string `json:"gecos,omitempty"` + Groups []Group `json:"groups,omitempty"` + HomeDir *string `json:"homeDir,omitempty"` + Name string `json:"name"` + NoCreateHome *bool `json:"noCreateHome,omitempty"` + NoLogInit *bool `json:"noLogInit,omitempty"` + NoUserGroup *bool `json:"noUserGroup,omitempty"` + PasswordHash *string `json:"passwordHash,omitempty"` + PrimaryGroup *string `json:"primaryGroup,omitempty"` + SSHAuthorizedKeys []SSHAuthorizedKey `json:"sshAuthorizedKeys,omitempty"` + Shell *string `json:"shell,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + System *bool `json:"system,omitempty"` + UID *int `json:"uid,omitempty"` +} + +type Proxy struct { + HTTPProxy *string `json:"httpProxy,omitempty"` + HTTPSProxy *string `json:"httpsProxy,omitempty"` + NoProxy []NoProxyItem `json:"noProxy,omitempty"` +} + +type Raid struct { + Devices []Device `json:"devices"` + Level string `json:"level"` + Name string `json:"name"` + Options []RaidOption `json:"options,omitempty"` + Spares *int `json:"spares,omitempty"` +} + +type RaidOption string + +type Resource struct { + Compression *string `json:"compression,omitempty"` + HTTPHeaders HTTPHeaders `json:"httpHeaders,omitempty"` + Source *string `json:"source,omitempty"` + Verification Verification `json:"verification,omitempty"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `json:"tls,omitempty"` +} + +type Storage struct { + Directories []Directory `json:"directories,omitempty"` + Disks []Disk `json:"disks,omitempty"` + Files []File `json:"files,omitempty"` + Filesystems []Filesystem `json:"filesystems,omitempty"` + Links []Link `json:"links,omitempty"` + Luks []Luks `json:"luks,omitempty"` + Raid []Raid `json:"raid,omitempty"` +} + +type Systemd struct { + Units []Unit `json:"units,omitempty"` +} + +type TLS struct { + CertificateAuthorities []Resource `json:"certificateAuthorities,omitempty"` +} + +type Tang struct { + Thumbprint *string `json:"thumbprint,omitempty"` + URL string `json:"url,omitempty"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `json:"httpResponseHeaders,omitempty"` + HTTPTotal *int `json:"httpTotal,omitempty"` +} + +type Unit struct { + Contents *string `json:"contents,omitempty"` + Dropins []Dropin `json:"dropins,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Mask *bool `json:"mask,omitempty"` + Name string `json:"name"` +} + +type Verification struct { + Hash *string `json:"hash,omitempty"` +} diff --git a/pkg/machine/keys.go b/pkg/machine/keys.go new file mode 100644 index 000000000..907e28f55 --- /dev/null +++ b/pkg/machine/keys.go @@ -0,0 +1,25 @@ +package machine + +import ( + "io/ioutil" + "os/exec" + "strings" +) + +// CreateSSHKeys makes a priv and pub ssh key for interacting +// the a VM. +func CreateSSHKeys(writeLocation string) (string, error) { + if err := generatekeys(writeLocation); err != nil { + return "", err + } + b, err := ioutil.ReadFile(writeLocation + ".pub") + if err != nil { + return "", err + } + return strings.TrimSuffix(string(b), "\n"), nil +} + +// generatekeys creates an ed25519 set of keys +func generatekeys(writeLocation string) error { + return exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", writeLocation).Run() +} diff --git a/pkg/machine/libvirt/config.go b/pkg/machine/libvirt/config.go new file mode 100644 index 000000000..903f15fbc --- /dev/null +++ b/pkg/machine/libvirt/config.go @@ -0,0 +1,4 @@ +package libvirt + +type MachineVM struct { +} diff --git a/pkg/machine/libvirt/machine.go b/pkg/machine/libvirt/machine.go new file mode 100644 index 000000000..2c907ba5f --- /dev/null +++ b/pkg/machine/libvirt/machine.go @@ -0,0 +1,15 @@ +package libvirt + +import "github.com/containers/podman/v3/pkg/machine" + +func (v *MachineVM) Create(name string, opts machine.CreateOptions) error { + return nil +} + +func (v *MachineVM) Start(name string) error { + return nil +} + +func (v *MachineVM) Stop(name string) error { + return nil +} diff --git a/pkg/machine/pull.go b/pkg/machine/pull.go new file mode 100644 index 000000000..39dde15b8 --- /dev/null +++ b/pkg/machine/pull.go @@ -0,0 +1,97 @@ +package machine + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "github.com/sirupsen/logrus" + "github.com/vbauerster/mpb/v6" + "github.com/vbauerster/mpb/v6/decor" +) + +// DownloadVMImage downloads a VM image from url to given path +// with download status +func DownloadVMImage(downloadURL fmt.Stringer, localImagePath string) error { + out, err := os.Create(localImagePath) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + logrus.Error(err) + } + }() + + resp, err := http.Get(downloadURL.String()) + if err != nil { + return err + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Error(err) + } + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("error downloading VM image: %s", resp.Status) + } + size := resp.ContentLength + urlSplit := strings.Split(downloadURL.String(), "/") + prefix := "Downloading VM image: " + urlSplit[len(urlSplit)-1] + onComplete := prefix + ": done" + + p := mpb.New( + mpb.WithWidth(60), + mpb.WithRefreshRate(180*time.Millisecond), + ) + + bar := p.AddBar(size, + mpb.BarFillerClearOnComplete(), + mpb.PrependDecorators( + decor.OnComplete(decor.Name(prefix), onComplete), + ), + mpb.AppendDecorators( + decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), ""), + ), + ) + + proxyReader := bar.ProxyReader(resp.Body) + defer func() { + if err := proxyReader.Close(); err != nil { + logrus.Error(err) + } + }() + + if _, err := io.Copy(out, proxyReader); err != nil { + return err + } + + p.Wait() + return nil +} + +// Will error out if file without .xz already exists +// Maybe extracting then renameing is a good idea here.. +// depends on xz: not pre-installed on mac, so it becomes a brew dependecy +func decompressXZ(src string, output io.Writer) error { + fmt.Println("Extracting compressed file") + cmd := exec.Command("xzcat", "-k", src) + //cmd := exec.Command("xz", "-d", "-k", "-v", src) + stdOut, err := cmd.StdoutPipe() + if err != nil { + return err + } + //cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + go func() { + if _, err := io.Copy(output, stdOut); err != nil { + logrus.Error(err) + } + }() + return cmd.Run() +} diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go new file mode 100644 index 000000000..e4687914d --- /dev/null +++ b/pkg/machine/qemu/config.go @@ -0,0 +1,43 @@ +package qemu + +import "time" + +type MachineVM struct { + // CPUs to be assigned to the VM + CPUs uint64 + // The command line representation of the qemu command + CmdLine []string + // IdentityPath is the fq path to the ssh priv key + IdentityPath string + // IgnitionFilePath is the fq path to the .ign file + IgnitionFilePath string + // ImagePath is the fq path to + ImagePath string + // Memory in megabytes assigned to the vm + Memory uint64 + // Name of the vm + Name string + // SSH port for user networking + Port int + // QMPMonitor is the qemu monitor object for sending commands + QMPMonitor Monitor + // RemoteUsername of the vm user + RemoteUsername string +} + +type Monitor struct { + // Address portion of the qmp monitor (/tmp/tmp.sock) + Address string + // Network portion of the qmp monitor (unix) + Network string + // Timeout in seconds for qmp monitor transactions + Timeout time.Duration +} + +var ( + // defaultQMPTimeout is the timeout duration for the + // qmp monitor interactions + defaultQMPTimeout time.Duration = 2 * time.Second + // defaultRemoteUser describes the ssh username default + defaultRemoteUser = "core" +) diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go new file mode 100644 index 000000000..92a16dda7 --- /dev/null +++ b/pkg/machine/qemu/machine.go @@ -0,0 +1,304 @@ +package qemu + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "time" + + "github.com/containers/podman/v3/pkg/machine" + "github.com/containers/podman/v3/pkg/specgen" + "github.com/containers/storage/pkg/homedir" + "github.com/digitalocean/go-qemu/qmp" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var ( + // vmtype refers to qemu (vs libvirt, krun, etc) + vmtype = "qemu" + // qemuCommon are the common command line arguments between the arches + //qemuCommon = []string{"-cpu", "host", "-qmp", "unix://tmp/qmp.sock,server,nowait"} + //qemuCommon = []string{"-cpu", "host", "-qmp", "tcp:localhost:4444,server,nowait"} +) + +// NewMachine creates an instance of a virtual machine based on the qemu +// virtualization. +func NewMachine(opts machine.CreateOptions) (machine.VM, error) { + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return nil, err + } + vm := new(MachineVM) + if len(opts.Name) > 0 { + vm.Name = opts.Name + } + vm.IgnitionFilePath = opts.IgnitionPath + // If no ignitionfilepath was provided, use defaults + if len(vm.IgnitionFilePath) < 1 { + ignitionFile := filepath.Join(vmConfigDir, vm.Name+".ign") + vm.IgnitionFilePath = ignitionFile + } + + // An image was specified + if len(opts.ImagePath) > 0 { + vm.ImagePath = opts.ImagePath + } + + // Assign remote user name. if not provided, use default + vm.RemoteUsername = opts.Username + if len(vm.RemoteUsername) < 1 { + vm.RemoteUsername = defaultRemoteUser + } + + // Add a random port for ssh + port, err := specgen.GetRandomPort() + if err != nil { + return nil, err + } + vm.Port = port + + vm.CPUs = opts.CPUS + vm.Memory = opts.Memory + + // Look up the executable + execPath, err := exec.LookPath(QemuCommand) + if err != nil { + return nil, err + } + cmd := append([]string{execPath}) + // Add memory + cmd = append(cmd, []string{"-m", strconv.Itoa(int(vm.Memory))}...) + // Add cpus + // TODO + // Add ignition file + cmd = append(cmd, []string{"-fw_cfg", "name=opt/com.coreos/config,file=" + vm.IgnitionFilePath}...) + // Add qmp socket + monitor, err := NewQMPMonitor("unix", vm.Name, defaultQMPTimeout) + if err != nil { + return nil, err + } + vm.QMPMonitor = monitor + cmd = append(cmd, []string{"-qmp", monitor.Network + ":/" + monitor.Address + ",server,nowait"}...) + + // Add network + cmd = append(cmd, "-nic", "user,model=virtio,hostfwd=tcp::"+strconv.Itoa(vm.Port)+"-:22") + vm.CmdLine = cmd + fmt.Println("///") + return vm, nil +} + +// LoadByName reads a json file that describes a known qemu vm +// and returns a vm instance +func LoadVMByName(name string) (machine.VM, error) { + // TODO need to define an error relating to ErrMachineNotFound + vm := new(MachineVM) + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return nil, err + } + b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json")) + if err != nil { + return nil, err + } + err = json.Unmarshal(b, vm) + logrus.Debug(vm.CmdLine) + return vm, err +} + +// Create writes the json configuration file to the filesystem for +// other verbs (start, stop) +func (v *MachineVM) Create(opts machine.CreateOptions) error { + sshDir := filepath.Join(homedir.Get(), ".ssh") + // GetConfDir creates the directory so no need to check for + // its existence + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return err + } + jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json" + v.IdentityPath = filepath.Join(sshDir, v.Name) + + dd, err := machine.NewFcosDownloader(vmtype, v.Name) + if err != nil { + return err + } + + v.ImagePath = dd.Get().LocalUncompressedFile + if err := dd.DownloadImage(); err != nil { + return err + } + // Add arch specific options including image location + v.CmdLine = append(v.CmdLine, v.addArchOptions()...) + + // Add location of bootable image + v.CmdLine = append(v.CmdLine, "-drive", "if=virtio,file="+v.ImagePath) + // This kind of stinks but no other way around this r/n + uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/user/1000/podman/podman.sock", strconv.Itoa(v.Port), v.RemoteUsername) + if err := machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { + return err + } + // Write the JSON file + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil { + return err + } + key, err := machine.CreateSSHKeys(v.IdentityPath) + if err != nil { + return err + } + // Run arch specific things that need to be done + if err := v.prepare(); err != nil { + return err + } + // Write the ignition file + return machine.NewIgnitionFile(opts.Username, key, v.IgnitionFilePath) +} + +// Start executes the qemu command line and forks it +func (v *MachineVM) Start(name string, _ machine.StartOptions) error { + var ( + err error + ) + attr := new(os.ProcAttr) + files := []*os.File{os.Stdin, os.Stdout, os.Stderr} + attr.Files = files + fmt.Print(v.CmdLine) + _, err = os.StartProcess(v.CmdLine[0], v.CmdLine, attr) + return err +} + +// Stop uses the qmp monitor to call a system_powerdown +func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { + // check if the qmp socket is there. if not, qemu instance is gone + if _, err := os.Stat(v.QMPMonitor.Address); 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) + if err != nil { + return err + } + // Simple JSON formation for the QAPI + stopCommand := struct { + Execute string `json:"execute"` + }{ + Execute: "system_powerdown", + } + input, err := json.Marshal(stopCommand) + if err != nil { + return err + } + if err := qmpMonitor.Connect(); err != nil { + return err + } + defer func() { + if err := qmpMonitor.Disconnect(); err != nil { + logrus.Error(err) + } + }() + _, err = qmpMonitor.Run(input) + return err +} + +// NewQMPMonitor creates the monitor subsection of our vm +func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) { + rtDir, err := getDataDir() + if err != nil { + return Monitor{}, err + } + if timeout == 0 { + timeout = defaultQMPTimeout + } + monitor := Monitor{ + Network: network, + Address: filepath.Join(rtDir, "podman", "qmp_"+name+".sock"), + Timeout: timeout, + } + return monitor, nil +} + +func (v *MachineVM) Destroy(name string, opts machine.DestroyOptions) (string, func() error, error) { + var ( + files []string + ) + + // cannot remove a running vm + if v.isRunning() { + return "", nil, errors.Errorf("running vm %q cannot be destroyed", v.Name) + } + + // Collect all the files that need to be destroyed + if !opts.SaveKeys { + files = append(files, v.IdentityPath, v.IdentityPath+".pub") + } + if !opts.SaveIgnition { + files = append(files, v.IgnitionFilePath) + } + if !opts.SaveImage { + files = append(files, v.ImagePath) + } + files = append(files, v.archRemovalFiles()...) + + if err := machine.RemoveConnection(v.Name); err != nil { + logrus.Error(err) + } + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return "", nil, err + } + files = append(files, filepath.Join(vmConfigDir, v.Name+".json")) + confirmationMessage := "\nThe following files will be deleted:\n\n" + for _, msg := range files { + confirmationMessage += msg + "\n" + } + confirmationMessage += "\n" + return confirmationMessage, func() error { + for _, f := range files { + if err := os.Remove(f); err != nil { + logrus.Error(err) + } + } + return nil + }, nil +} + +func (v *MachineVM) isRunning() bool { + // Check if qmp socket path exists + if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) { + return false + } + // Check if we can dial it + if _, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout); err != nil { + return false + } + return true +} + +// SSH opens an interactive SSH session to the vm specified. +// Added ssh function to VM interface: pkg/machine/config/go : line 58 +func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error { + if !v.isRunning() { + return errors.Errorf("vm %q is not running.", v.Name) + } + + sshDestination := v.RemoteUsername + "@localhost" + port := strconv.Itoa(v.Port) + + fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name) + + cmd := exec.Command("ssh", "-i", v.IdentityPath, "-p", port, sshDestination) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} diff --git a/pkg/machine/qemu/options_amd64.go b/pkg/machine/qemu/options_amd64.go new file mode 100644 index 000000000..85a2c4b3e --- /dev/null +++ b/pkg/machine/qemu/options_amd64.go @@ -0,0 +1,26 @@ +package qemu + +import ( + "github.com/containers/podman/v3/pkg/util" +) + +var ( + QemuCommand = "qemu-kvm" +) + +func (v *MachineVM) addArchOptions() []string { + opts := []string{"-cpu", "host"} + return opts +} + +func (v *MachineVM) prepare() error { + return nil +} + +func (v *MachineVM) archRemovalFiles() []string { + return []string{} +} + +func getDataDir() (string, error) { + return util.GetRuntimeDir() +} diff --git a/pkg/machine/qemu/options_arm64.go b/pkg/machine/qemu/options_arm64.go new file mode 100644 index 000000000..c5b0ea16b --- /dev/null +++ b/pkg/machine/qemu/options_arm64.go @@ -0,0 +1,40 @@ +package qemu + +import ( + "os/exec" + "path/filepath" +) + +var ( + QemuCommand = "qemu-system-aarch64" +) + +func (v *MachineVM) addArchOptions() []string { + ovmfDir := getOvmfDir(v.ImagePath, v.Name) + opts := []string{ + "-accel", "hvf", + "-cpu", "cortex-a57", + "-M", "virt,highmem=off", + "-drive", "file=/usr/local/share/qemu/edk2-aarch64-code.fd,if=pflash,format=raw,readonly=on", + "-drive", "file=" + ovmfDir + ",if=pflash,format=raw"} + return opts +} + +func (v *MachineVM) prepare() error { + ovmfDir := getOvmfDir(v.ImagePath, v.Name) + cmd := []string{"dd", "if=/dev/zero", "conv=sync", "bs=1m", "count=64", "of=" + ovmfDir} + return exec.Command(cmd[0], cmd[1:]...).Run() +} + +func (v *MachineVM) archRemovalFiles() []string { + ovmDir := getOvmfDir(v.ImagePath, v.Name) + return []string{ovmDir} +} + +func getDataDir() (string, error) { + return "/tmp", nil +} + +func getOvmfDir(imagePath, vmName string) string { + return filepath.Join(filepath.Dir(imagePath), vmName+"_ovmf_vars.fd") +} -- cgit v1.2.3-54-g00ecf From e7661137373b5f87bf6ec45e32326821b172ce7b Mon Sep 17 00:00:00 2001 From: Ashley Cui Date: Mon, 22 Mar 2021 13:23:22 -0400 Subject: Add --execute flag to podman machine ssh --execute, -e allows to execute a command through ssh Signed-off-by: Ashley Cui --- cmd/podman/machine/ssh.go | 36 ++++++++++++++++++++++------ docs/source/markdown/podman-machine-ssh.1.md | 2 +- pkg/machine/config.go | 5 +++- pkg/machine/qemu/machine.go | 9 +++++-- 4 files changed, 41 insertions(+), 11 deletions(-) (limited to 'pkg/machine') diff --git a/cmd/podman/machine/ssh.go b/cmd/podman/machine/ssh.go index a0656d17b..32483f731 100644 --- a/cmd/podman/machine/ssh.go +++ b/cmd/podman/machine/ssh.go @@ -12,22 +12,33 @@ import ( var ( sshCmd = &cobra.Command{ - Use: "ssh NAME", - Short: "SSH into a virtual machine", - Long: "SSH into a podman-managed virtual machine ", - RunE: ssh, - Args: cobra.ExactArgs(1), - Example: `podman machine ssh myvm`, + Use: "ssh [options] NAME [COMMAND [ARG ...]]", + Short: "SSH into a virtual machine", + Long: "SSH into a podman-managed virtual machine ", + RunE: ssh, + Args: cobra.MinimumNArgs(1), + Example: `podman machine ssh myvm + podman machine ssh -e myvm echo hello`, + ValidArgsFunction: completion.AutocompleteNone, } ) +var ( + sshOpts machine.SSHOptions +) + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Command: sshCmd, Parent: machineCmd, }) + + flags := sshCmd.Flags() + executeFlagName := "execute" + flags.BoolVarP(&sshOpts.Execute, executeFlagName, "e", false, "Execute command from args") + _ = sshCmd.RegisterFlagCompletionFunc(executeFlagName, completion.AutocompleteDefault) } func ssh(cmd *cobra.Command, args []string) error { @@ -36,6 +47,17 @@ func ssh(cmd *cobra.Command, args []string) error { vm machine.VM vmType string ) + sshOpts.Args = args[1:] + + // Error if no execute but args given + if !sshOpts.Execute && len(sshOpts.Args) > 0 { + return errors.New("too many args: to execute commands via ssh, use -e flag") + } + // Error if execute but no args given + if sshOpts.Execute && len(sshOpts.Args) < 1 { + return errors.New("must proivde at least one command to execute") + } + switch vmType { default: vm, err = qemu.LoadVMByName(args[0]) @@ -43,5 +65,5 @@ func ssh(cmd *cobra.Command, args []string) error { if err != nil { return errors.Wrapf(err, "vm %s not found", args[0]) } - return vm.SSH(args[0], machine.SSHOptions{}) + return vm.SSH(args[0], sshOpts) } diff --git a/docs/source/markdown/podman-machine-ssh.1.md b/docs/source/markdown/podman-machine-ssh.1.md index c0679347e..ed35a38e4 100644 --- a/docs/source/markdown/podman-machine-ssh.1.md +++ b/docs/source/markdown/podman-machine-ssh.1.md @@ -4,7 +4,7 @@ podman\-machine\-ssh - SSH into a virtual machine ## SYNOPSIS -**podman machine ssh** *name* +**podman machine ssh** [*options*] *name* [*command* [*arg* ...]] ## DESCRIPTION diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 5e90dae51..2a70b8ff7 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -42,7 +42,10 @@ type Download struct { VMName string } -type SSHOptions struct{} +type SSHOptions struct { + Execute bool + Args []string +} type StartOptions struct{} type StopOptions struct{} diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 92a16dda7..30d96ce08 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -293,9 +293,14 @@ func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error { sshDestination := v.RemoteUsername + "@localhost" port := strconv.Itoa(v.Port) - fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name) + args := []string{"-i", v.IdentityPath, "-p", port, sshDestination} + if opts.Execute { + args = append(args, opts.Args...) + } else { + fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name) + } - cmd := exec.Command("ssh", "-i", v.IdentityPath, "-p", port, sshDestination) + cmd := exec.Command("ssh", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin -- cgit v1.2.3-54-g00ecf From 4ab8a6f67eb9de0de40d478cb0cbec05b1b725c0 Mon Sep 17 00:00:00 2001 From: baude Date: Mon, 22 Mar 2021 13:29:25 -0500 Subject: Improvements for machine clean up ci failures and add appropriate arch,os exclusion tags Signed-off-by: baude --- cmd/podman/common/completion.go | 8 +++ cmd/podman/machine/create.go | 28 +++----- cmd/podman/machine/destroy.go | 91 ------------------------ cmd/podman/machine/machine.go | 4 +- cmd/podman/machine/machine_unsupported.go | 5 ++ cmd/podman/machine/remove.go | 88 +++++++++++++++++++++++ cmd/podman/machine/ssh.go | 9 +-- cmd/podman/machine/start.go | 2 + cmd/podman/machine/stop.go | 2 + docs/source/machine.rst | 2 +- docs/source/markdown/podman-machine-create.1.md | 6 +- docs/source/markdown/podman-machine-destroy.1.md | 65 ----------------- docs/source/markdown/podman-machine-remove.1.md | 65 +++++++++++++++++ docs/source/markdown/podman-machine-ssh.1.md | 10 +++ docs/source/markdown/podman-machine.1.md | 2 +- pkg/bindings/connection.go | 12 +--- pkg/machine/config.go | 37 +++------- pkg/machine/fcos.go | 4 +- pkg/machine/qemu/machine.go | 20 ++++-- pkg/machine/qemu/options_amd64.go | 26 ------- pkg/machine/qemu/options_arm64.go | 40 ----------- pkg/machine/qemu/options_darwin.go | 15 ++++ pkg/machine/qemu/options_darwin_amd64.go | 18 +++++ pkg/machine/qemu/options_darwin_arm64.go | 36 ++++++++++ pkg/machine/qemu/options_linux.go | 7 ++ pkg/machine/qemu/options_linux_amd64.go | 18 +++++ pkg/specgen/generate/ports.go | 6 +- pkg/specgen/ports.go | 26 ------- utils/ports.go | 26 +++++++ 29 files changed, 352 insertions(+), 326 deletions(-) delete mode 100644 cmd/podman/machine/destroy.go create mode 100644 cmd/podman/machine/machine_unsupported.go create mode 100644 cmd/podman/machine/remove.go delete mode 100644 docs/source/markdown/podman-machine-destroy.1.md create mode 100644 docs/source/markdown/podman-machine-remove.1.md delete mode 100644 pkg/machine/qemu/options_amd64.go delete mode 100644 pkg/machine/qemu/options_arm64.go create mode 100644 pkg/machine/qemu/options_darwin.go create mode 100644 pkg/machine/qemu/options_darwin_amd64.go create mode 100644 pkg/machine/qemu/options_darwin_arm64.go create mode 100644 pkg/machine/qemu/options_linux.go create mode 100644 pkg/machine/qemu/options_linux_amd64.go delete mode 100644 pkg/specgen/ports.go create mode 100644 utils/ports.go (limited to 'pkg/machine') diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index bc106263c..6bed5e0c6 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1092,3 +1092,11 @@ func AutocompleteVolumeFilters(cmd *cobra.Command, args []string, toComplete str } return completeKeyValues(toComplete, kv) } + +// AutocompleteMachineSSH - Autocomplete machine ssh command. +func AutocompleteMachineSSH(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveDefault +} diff --git a/cmd/podman/machine/create.go b/cmd/podman/machine/create.go index 04c5e9e65..1da34327a 100644 --- a/cmd/podman/machine/create.go +++ b/cmd/podman/machine/create.go @@ -1,3 +1,5 @@ +// +build amd64,linux amd64,darwin arm64,darwin + package machine import ( @@ -6,17 +8,16 @@ import ( "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/machine" "github.com/containers/podman/v3/pkg/machine/qemu" - "github.com/pkg/errors" "github.com/spf13/cobra" ) var ( createCmd = &cobra.Command{ - Use: "create [options] NAME", + Use: "create [options] [NAME]", Short: "Create a vm", - Long: "Create a virtual machine for Podman to run on. Virtual machines are used to run Podman on Macs. ", + Long: "Create a virtual machine for Podman to run on. Virtual machines are used to run Podman.", RunE: create, - Args: cobra.NoArgs, + Args: cobra.MaximumNArgs(1), Example: `podman machine create myvm`, ValidArgsFunction: completion.AutocompleteNone, } @@ -32,7 +33,8 @@ type CreateCLIOptions struct { } var ( - createOpts = CreateCLIOptions{} + createOpts = CreateCLIOptions{} + defaultMachineName string = "podman-machine-default" ) func init() { @@ -59,14 +61,6 @@ func init() { ) _ = createCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone) - deviceFlagName := "name" - flags.StringVar( - &createOpts.Name, - deviceFlagName, "", - "set vm name", - ) - _ = createCmd.RegisterFlagCompletionFunc(deviceFlagName, completion.AutocompleteDefault) - ImagePathFlagName := "image-path" flags.StringVar(&createOpts.ImagePath, ImagePathFlagName, "", "Path to qcow image") _ = createCmd.RegisterFlagCompletionFunc(ImagePathFlagName, completion.AutocompleteDefault) @@ -78,9 +72,9 @@ func init() { // TODO should we allow for a users to append to the qemu cmdline? func create(cmd *cobra.Command, args []string) error { - // TODO add ability to create default, not name required - if len(createOpts.Name) < 1 { - return errors.New("required --name not provided") + createOpts.Name = defaultMachineName + if len(args) > 0 { + createOpts.Name = args[0] } vmOpts := machine.CreateOptions{ CPUS: createOpts.CPUS, @@ -95,8 +89,6 @@ func create(cmd *cobra.Command, args []string) error { err error ) switch vmType { - case "foobar": - // do nothing default: // qemu is the default vm, err = qemu.NewMachine(vmOpts) } diff --git a/cmd/podman/machine/destroy.go b/cmd/podman/machine/destroy.go deleted file mode 100644 index ab23d607d..000000000 --- a/cmd/podman/machine/destroy.go +++ /dev/null @@ -1,91 +0,0 @@ -package machine - -import ( - "bufio" - "fmt" - "os" - "strings" - - "github.com/containers/common/pkg/completion" - "github.com/containers/podman/v3/cmd/podman/common" - "github.com/containers/podman/v3/cmd/podman/registry" - "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" - "github.com/spf13/cobra" -) - -var ( - destroyCmd = &cobra.Command{ - Use: "destroy [options] NAME", - Short: "Destroy an existing machine", - Long: "Destroy an existing machine ", - RunE: destroy, - Args: cobra.ExactArgs(1), - Example: `podman machine destroy myvm`, - ValidArgsFunction: completion.AutocompleteNone, - } -) - -var ( - destoryOptions machine.DestroyOptions -) - -func init() { - registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, - Command: destroyCmd, - Parent: machineCmd, - }) - - flags := destroyCmd.Flags() - formatFlagName := "force" - flags.BoolVar(&destoryOptions.Force, formatFlagName, false, "Do not prompt before destroying") - _ = destroyCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat) - - keysFlagName := "save-keys" - flags.BoolVar(&destoryOptions.SaveKeys, keysFlagName, false, "Do not delete SSH keys") - _ = destroyCmd.RegisterFlagCompletionFunc(keysFlagName, common.AutocompleteJSONFormat) - - ignitionFlagName := "save-ignition" - flags.BoolVar(&destoryOptions.SaveIgnition, ignitionFlagName, false, "Do not delete ignition file") - _ = destroyCmd.RegisterFlagCompletionFunc(ignitionFlagName, common.AutocompleteJSONFormat) - - imageFlagName := "save-image" - flags.BoolVar(&destoryOptions.SaveImage, imageFlagName, false, "Do not delete the image file") - _ = destroyCmd.RegisterFlagCompletionFunc(imageFlagName, common.AutocompleteJSONFormat) -} - -func destroy(cmd *cobra.Command, args []string) error { - var ( - err error - vm machine.VM - vmType string - ) - switch vmType { - default: - vm, err = qemu.LoadVMByName(args[0]) - } - if err != nil { - return err - } - confirmationMessage, doIt, err := vm.Destroy(args[0], machine.DestroyOptions{}) - if err != nil { - return err - } - - if !destoryOptions.Force { - // Warn user - fmt.Println(confirmationMessage) - reader := bufio.NewReader(os.Stdin) - fmt.Print("Are you sure you want to continue? [y/N] ") - answer, err := reader.ReadString('\n') - if err != nil { - return err - } - if strings.ToLower(answer)[0] != 'y' { - return nil - } - } - return doIt() -} diff --git a/cmd/podman/machine/machine.go b/cmd/podman/machine/machine.go index ce5a3b889..ed284ee10 100644 --- a/cmd/podman/machine/machine.go +++ b/cmd/podman/machine/machine.go @@ -1,3 +1,5 @@ +// +build amd64,linux amd64,darwin arm64,darwin + package machine import ( @@ -15,7 +17,7 @@ var ( machineCmd = &cobra.Command{ Use: "machine", Short: "Manage a virtual machine", - Long: "Manage a virtual machine. Virtual machines are used to run Podman on Macs.", + Long: "Manage a virtual machine. Virtual machines are used to run Podman.", PersistentPreRunE: noOp, PersistentPostRunE: noOp, RunE: validate.SubCommandExists, diff --git a/cmd/podman/machine/machine_unsupported.go b/cmd/podman/machine/machine_unsupported.go new file mode 100644 index 000000000..cb1636419 --- /dev/null +++ b/cmd/podman/machine/machine_unsupported.go @@ -0,0 +1,5 @@ +// +build !amd64 arm64,linux amd64,windows + +package machine + +func init() {} diff --git a/cmd/podman/machine/remove.go b/cmd/podman/machine/remove.go new file mode 100644 index 000000000..f6ce9e326 --- /dev/null +++ b/cmd/podman/machine/remove.go @@ -0,0 +1,88 @@ +// +build amd64,linux amd64,darwin arm64,darwin + +package machine + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v3/cmd/podman/registry" + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/pkg/machine" + "github.com/containers/podman/v3/pkg/machine/qemu" + "github.com/spf13/cobra" +) + +var ( + removeCmd = &cobra.Command{ + Use: "remove [options] NAME", + Short: "Remove an existing machine", + Long: "Remove an existing machine ", + RunE: remove, + Args: cobra.ExactArgs(1), + Example: `podman machine remove myvm`, + ValidArgsFunction: completion.AutocompleteNone, + } +) + +var ( + destoryOptions machine.RemoveOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: removeCmd, + Parent: machineCmd, + }) + + flags := removeCmd.Flags() + formatFlagName := "force" + flags.BoolVar(&destoryOptions.Force, formatFlagName, false, "Do not prompt before removeing") + + keysFlagName := "save-keys" + flags.BoolVar(&destoryOptions.SaveKeys, keysFlagName, false, "Do not delete SSH keys") + + ignitionFlagName := "save-ignition" + flags.BoolVar(&destoryOptions.SaveIgnition, ignitionFlagName, false, "Do not delete ignition file") + + imageFlagName := "save-image" + flags.BoolVar(&destoryOptions.SaveImage, imageFlagName, false, "Do not delete the image file") +} + +func remove(cmd *cobra.Command, args []string) error { + var ( + err error + vm machine.VM + vmType string + ) + switch vmType { + default: + vm, err = qemu.LoadVMByName(args[0]) + } + if err != nil { + return err + } + confirmationMessage, doIt, err := vm.Remove(args[0], machine.RemoveOptions{}) + if err != nil { + return err + } + + if !destoryOptions.Force { + // Warn user + fmt.Println(confirmationMessage) + reader := bufio.NewReader(os.Stdin) + fmt.Print("Are you sure you want to continue? [y/N] ") + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + return doIt() +} diff --git a/cmd/podman/machine/ssh.go b/cmd/podman/machine/ssh.go index 32483f731..a7111a195 100644 --- a/cmd/podman/machine/ssh.go +++ b/cmd/podman/machine/ssh.go @@ -1,7 +1,9 @@ +// +build amd64,linux amd64,darwin arm64,darwin + package machine import ( - "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/machine" @@ -14,13 +16,13 @@ var ( sshCmd = &cobra.Command{ Use: "ssh [options] NAME [COMMAND [ARG ...]]", Short: "SSH into a virtual machine", - Long: "SSH into a podman-managed virtual machine ", + Long: "SSH into a virtual machine ", RunE: ssh, Args: cobra.MinimumNArgs(1), Example: `podman machine ssh myvm podman machine ssh -e myvm echo hello`, - ValidArgsFunction: completion.AutocompleteNone, + ValidArgsFunction: common.AutocompleteMachineSSH, } ) @@ -38,7 +40,6 @@ func init() { flags := sshCmd.Flags() executeFlagName := "execute" flags.BoolVarP(&sshOpts.Execute, executeFlagName, "e", false, "Execute command from args") - _ = sshCmd.RegisterFlagCompletionFunc(executeFlagName, completion.AutocompleteDefault) } func ssh(cmd *cobra.Command, args []string) error { diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 762639358..44ade2850 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -1,3 +1,5 @@ +// +build amd64,linux amd64,darwin arm64,darwin + package machine import ( diff --git a/cmd/podman/machine/stop.go b/cmd/podman/machine/stop.go index b6585c296..35fd4ff95 100644 --- a/cmd/podman/machine/stop.go +++ b/cmd/podman/machine/stop.go @@ -1,3 +1,5 @@ +// +build amd64,linux amd64,darwin arm64,darwin + package machine import ( diff --git a/docs/source/machine.rst b/docs/source/machine.rst index aefceb3a5..cf7f72cce 100644 --- a/docs/source/machine.rst +++ b/docs/source/machine.rst @@ -3,7 +3,7 @@ Machine :doc:`create ` Create a new virtual machine -:doc:`destroy ` Destroy a virtual machine +:doc:`remove ` Remove a virtual machine :doc:`ssh ` SSH into a virtual machine :doc:`start ` Start a virtual machine :doc:`stop ` Stop a virtual machine diff --git a/docs/source/markdown/podman-machine-create.1.md b/docs/source/markdown/podman-machine-create.1.md index 1e199bb77..eb498f2dd 100644 --- a/docs/source/markdown/podman-machine-create.1.md +++ b/docs/source/markdown/podman-machine-create.1.md @@ -4,7 +4,7 @@ podman\-machine\-create - Create a new virtual machine ## SYNOPSIS -**podman machine create** [*options*] *name* +**podman machine create** [*options*] [*name*] ## DESCRIPTION @@ -34,10 +34,6 @@ Fully qualified path of the uncompressed image file Memory (in MB). -#### **--name** - -Name to assign to the VM - #### **--help** Print usage statement. diff --git a/docs/source/markdown/podman-machine-destroy.1.md b/docs/source/markdown/podman-machine-destroy.1.md deleted file mode 100644 index 7c5421418..000000000 --- a/docs/source/markdown/podman-machine-destroy.1.md +++ /dev/null @@ -1,65 +0,0 @@ -% podman-machine-destroy(1) - -## NAME -podman\-machine\-destroy - Destroy a virtual machine - -## SYNOPSIS -**podman machine destroy** [*options*] *name* - -## DESCRIPTION - -Destroy a virtual machine and its related files. What is actually deleted -depends on the virtual machine type. For all virtual machines, the generated -SSH keys and the podman system connection are deleted. The ignition files -generated for that VM are also destroyed as is its image file on the filesystem. - -Users get a display of what will be deleted and are required to confirm unless the option `--force` -is used. - - -## OPTIONS - -#### **--help** - -Print usage statement. - -#### **--force** - -Delete without confirmation - -#### **--save-ignition** - -Do not delete the generated ignition file - -#### **--save-image** - -Do not delete the VM image - -#### **--save-keys** - -Do not delete the SSH keys for the VM. The system connection is always -deleted. - -## EXAMPLES - -Destroy a VM named "test1" - -``` -$ podman machine destroy test1 - -The following files will be deleted: - -/home/user/.ssh/test1 -/home/user/.ssh/test1.pub -/home/user/.config/containers/podman/machine/qemu/test1.ign -/home/user/.local/share/containers/podman/machine/qemu/test1_fedora-coreos-33.20210315.1.0-qemu.x86_64.qcow2 -/home/user/.config/containers/podman/machine/qemu/test1.json - -Are you sure you want to continue? [y/N] y -``` - -## SEE ALSO -podman-machine (1) - -## HISTORY -March 2021, Originally compiled by Ashley Cui diff --git a/docs/source/markdown/podman-machine-remove.1.md b/docs/source/markdown/podman-machine-remove.1.md new file mode 100644 index 000000000..07763741d --- /dev/null +++ b/docs/source/markdown/podman-machine-remove.1.md @@ -0,0 +1,65 @@ +% podman-machine-remove(1) + +## NAME +podman\-machine\-remove - Remove a virtual machine + +## SYNOPSIS +**podman machine remove** [*options*] *name* + +## DESCRIPTION + +Remove a virtual machine and its related files. What is actually deleted +depends on the virtual machine type. For all virtual machines, the generated +SSH keys and the podman system connection are deleted. The ignition files +generated for that VM are also removeed as is its image file on the filesystem. + +Users get a display of what will be deleted and are required to confirm unless the option `--force` +is used. + + +## OPTIONS + +#### **--help** + +Print usage statement. + +#### **--force** + +Delete without confirmation + +#### **--save-ignition** + +Do not delete the generated ignition file + +#### **--save-image** + +Do not delete the VM image + +#### **--save-keys** + +Do not delete the SSH keys for the VM. The system connection is always +deleted. + +## EXAMPLES + +Remove a VM named "test1" + +``` +$ podman machine remove test1 + +The following files will be deleted: + +/home/user/.ssh/test1 +/home/user/.ssh/test1.pub +/home/user/.config/containers/podman/machine/qemu/test1.ign +/home/user/.local/share/containers/podman/machine/qemu/test1_fedora-coreos-33.20210315.1.0-qemu.x86_64.qcow2 +/home/user/.config/containers/podman/machine/qemu/test1.json + +Are you sure you want to continue? [y/N] y +``` + +## SEE ALSO +podman-machine (1) + +## HISTORY +March 2021, Originally compiled by Ashley Cui diff --git a/docs/source/markdown/podman-machine-ssh.1.md b/docs/source/markdown/podman-machine-ssh.1.md index ed35a38e4..bcecd1010 100644 --- a/docs/source/markdown/podman-machine-ssh.1.md +++ b/docs/source/markdown/podman-machine-ssh.1.md @@ -16,16 +16,26 @@ tied to the Linux kernel. ## OPTIONS +#### **--execute**, **-e** + +Execute the given command on the VM + #### **--help** Print usage statement. ## EXAMPLES +To get an interactive session with a VM called `myvm`: ``` $ podman machine ssh myvm ``` +To run a command on a VM called `myvm`: +``` +$ podman machine ssh -e myvm -- rpm -q podman +``` + ## SEE ALSO podman-machine (1) diff --git a/docs/source/markdown/podman-machine.1.md b/docs/source/markdown/podman-machine.1.md index b31d8f788..52f212cdd 100644 --- a/docs/source/markdown/podman-machine.1.md +++ b/docs/source/markdown/podman-machine.1.md @@ -14,7 +14,7 @@ podman\-machine - Manage Podman's virtual machine | Command | Man Page | Description | | ------- | ------------------------------------------------------- | ----------------------------- | | create | [podman-machine-create(1)](podman-machine-create.1.md) | Create a new virtual machine | -| destroy | [podman-machine-destroy(1)](podman-machine-destroy.1.md)| Destroy a virtual machine | +| remove | [podman-machine-destroy(1)](podman-machine-remove.1.md)| Remove a virtual machine | | ssh | [podman-machine-ssh.1.md(1)](podman-machine-ssh.1.md) | SSH into a virtual machine | | start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine | | stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine | diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 148b592e9..21a8e7a8b 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -22,21 +22,11 @@ import ( "golang.org/x/crypto/ssh/agent" ) -/* - WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING - WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING - - DO NOT MERGE WITHOUT REVERTING THE HACK BELOW - - WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING - WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING -*/ var ( BasePath = &url.URL{ Scheme: "http", Host: "d", - //Path: "/v" + version.APIVersion[version.Libpod][version.CurrentAPI].String() + "/libpod", - Path: "/v3.0.0/libpod", + Path: "/v" + version.APIVersion[version.Libpod][version.CurrentAPI].String() + "/libpod", } ) diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 2a70b8ff7..242401ab4 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -50,7 +50,7 @@ type StartOptions struct{} type StopOptions struct{} -type DestroyOptions struct { +type RemoveOptions struct { Force bool SaveKeys bool SaveImage bool @@ -59,7 +59,7 @@ type DestroyOptions struct { type VM interface { Create(opts CreateOptions) error - Destroy(name string, opts DestroyOptions) (string, func() error, error) + Remove(name string, opts RemoveOptions) (string, func() error, error) SSH(name string, opts SSHOptions) error Start(name string, opts StartOptions) error Stop(name string, opts StopOptions) error @@ -70,33 +70,18 @@ type DistributionDownload interface { Get() *Download } -// TODO is this even needed? -type TestVM struct{} - -func (vm *TestVM) Create(opts CreateOptions) error { - return nil -} - -func (vm *TestVM) Start(name string, opts StartOptions) error { - return nil -} -func (vm *TestVM) Stop(name string, opts StopOptions) error { - return nil -} - func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL { userInfo := url.User(userName) uri := url.URL{ - Scheme: "ssh", - Opaque: "", - User: userInfo, - Host: host, - Path: path, - RawPath: "", - ForceQuery: false, - RawQuery: "", - Fragment: "", - RawFragment: "", + 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) diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index 8bbad458f..0c6a2485e 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -11,10 +11,8 @@ import ( "strings" "github.com/containers/storage/pkg/archive" - - "github.com/sirupsen/logrus" - digest "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" ) // These should eventually be moved into machine/qemu as diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 30d96ce08..504b64bd5 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -10,8 +10,9 @@ import ( "strconv" "time" + "github.com/containers/podman/v3/utils" + "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/storage/pkg/homedir" "github.com/digitalocean/go-qemu/qmp" "github.com/pkg/errors" @@ -56,7 +57,7 @@ func NewMachine(opts machine.CreateOptions) (machine.VM, error) { } // Add a random port for ssh - port, err := specgen.GetRandomPort() + port, err := utils.GetRandomPort() if err != nil { return nil, err } @@ -170,7 +171,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { attr := new(os.ProcAttr) files := []*os.File{os.Stdin, os.Stdout, os.Stderr} attr.Files = files - fmt.Print(v.CmdLine) + logrus.Debug(v.CmdLine) _, err = os.StartProcess(v.CmdLine[0], v.CmdLine, attr) return err } @@ -211,22 +212,29 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { // NewQMPMonitor creates the monitor subsection of our vm func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) { - rtDir, err := getDataDir() + rtDir, err := getSocketDir() if err != nil { return Monitor{}, err } + rtDir = filepath.Join(rtDir, "podman") + if _, err := os.Stat(filepath.Join(rtDir)); os.IsNotExist(err) { + // TODO 0644 is fine on linux but macos is weird + if err := os.MkdirAll(rtDir, 0755); err != nil { + return Monitor{}, err + } + } if timeout == 0 { timeout = defaultQMPTimeout } monitor := Monitor{ Network: network, - Address: filepath.Join(rtDir, "podman", "qmp_"+name+".sock"), + Address: filepath.Join(rtDir, "qmp_"+name+".sock"), Timeout: timeout, } return monitor, nil } -func (v *MachineVM) Destroy(name string, opts machine.DestroyOptions) (string, func() error, error) { +func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { var ( files []string ) diff --git a/pkg/machine/qemu/options_amd64.go b/pkg/machine/qemu/options_amd64.go deleted file mode 100644 index 85a2c4b3e..000000000 --- a/pkg/machine/qemu/options_amd64.go +++ /dev/null @@ -1,26 +0,0 @@ -package qemu - -import ( - "github.com/containers/podman/v3/pkg/util" -) - -var ( - QemuCommand = "qemu-kvm" -) - -func (v *MachineVM) addArchOptions() []string { - opts := []string{"-cpu", "host"} - return opts -} - -func (v *MachineVM) prepare() error { - return nil -} - -func (v *MachineVM) archRemovalFiles() []string { - return []string{} -} - -func getDataDir() (string, error) { - return util.GetRuntimeDir() -} diff --git a/pkg/machine/qemu/options_arm64.go b/pkg/machine/qemu/options_arm64.go deleted file mode 100644 index c5b0ea16b..000000000 --- a/pkg/machine/qemu/options_arm64.go +++ /dev/null @@ -1,40 +0,0 @@ -package qemu - -import ( - "os/exec" - "path/filepath" -) - -var ( - QemuCommand = "qemu-system-aarch64" -) - -func (v *MachineVM) addArchOptions() []string { - ovmfDir := getOvmfDir(v.ImagePath, v.Name) - opts := []string{ - "-accel", "hvf", - "-cpu", "cortex-a57", - "-M", "virt,highmem=off", - "-drive", "file=/usr/local/share/qemu/edk2-aarch64-code.fd,if=pflash,format=raw,readonly=on", - "-drive", "file=" + ovmfDir + ",if=pflash,format=raw"} - return opts -} - -func (v *MachineVM) prepare() error { - ovmfDir := getOvmfDir(v.ImagePath, v.Name) - cmd := []string{"dd", "if=/dev/zero", "conv=sync", "bs=1m", "count=64", "of=" + ovmfDir} - return exec.Command(cmd[0], cmd[1:]...).Run() -} - -func (v *MachineVM) archRemovalFiles() []string { - ovmDir := getOvmfDir(v.ImagePath, v.Name) - return []string{ovmDir} -} - -func getDataDir() (string, error) { - return "/tmp", nil -} - -func getOvmfDir(imagePath, vmName string) string { - return filepath.Join(filepath.Dir(imagePath), vmName+"_ovmf_vars.fd") -} diff --git a/pkg/machine/qemu/options_darwin.go b/pkg/machine/qemu/options_darwin.go new file mode 100644 index 000000000..46ccf24cb --- /dev/null +++ b/pkg/machine/qemu/options_darwin.go @@ -0,0 +1,15 @@ +package qemu + +import ( + "os" + + "github.com/pkg/errors" +) + +func getSocketDir() (string, error) { + tmpDir, ok := os.LookupEnv("TMPDIR") + if !ok { + return "", errors.New("unable to resolve TMPDIR") + } + return tmpDir, nil +} diff --git a/pkg/machine/qemu/options_darwin_amd64.go b/pkg/machine/qemu/options_darwin_amd64.go new file mode 100644 index 000000000..69f7982b2 --- /dev/null +++ b/pkg/machine/qemu/options_darwin_amd64.go @@ -0,0 +1,18 @@ +package qemu + +var ( + QemuCommand = "qemu-system-x86_64" +) + +func (v *MachineVM) addArchOptions() []string { + opts := []string{"-cpu", "host"} + return opts +} + +func (v *MachineVM) prepare() error { + return nil +} + +func (v *MachineVM) archRemovalFiles() []string { + return []string{} +} diff --git a/pkg/machine/qemu/options_darwin_arm64.go b/pkg/machine/qemu/options_darwin_arm64.go new file mode 100644 index 000000000..7513b3048 --- /dev/null +++ b/pkg/machine/qemu/options_darwin_arm64.go @@ -0,0 +1,36 @@ +package qemu + +import ( + "os/exec" + "path/filepath" +) + +var ( + QemuCommand = "qemu-system-aarch64" +) + +func (v *MachineVM) addArchOptions() []string { + ovmfDir := getOvmfDir(v.ImagePath, v.Name) + opts := []string{ + "-accel", "hvf", + "-cpu", "cortex-a57", + "-M", "virt,highmem=off", + "-drive", "file=/usr/local/share/qemu/edk2-aarch64-code.fd,if=pflash,format=raw,readonly=on", + "-drive", "file=" + ovmfDir + ",if=pflash,format=raw"} + return opts +} + +func (v *MachineVM) prepare() error { + ovmfDir := getOvmfDir(v.ImagePath, v.Name) + cmd := []string{"dd", "if=/dev/zero", "conv=sync", "bs=1m", "count=64", "of=" + ovmfDir} + return exec.Command(cmd[0], cmd[1:]...).Run() +} + +func (v *MachineVM) archRemovalFiles() []string { + ovmDir := getOvmfDir(v.ImagePath, v.Name) + return []string{ovmDir} +} + +func getOvmfDir(imagePath, vmName string) string { + return filepath.Join(filepath.Dir(imagePath), vmName+"_ovmf_vars.fd") +} diff --git a/pkg/machine/qemu/options_linux.go b/pkg/machine/qemu/options_linux.go new file mode 100644 index 000000000..0a2e40d8f --- /dev/null +++ b/pkg/machine/qemu/options_linux.go @@ -0,0 +1,7 @@ +package qemu + +import "github.com/containers/podman/v3/pkg/util" + +func getSocketDir() (string, error) { + return util.GetRuntimeDir() +} diff --git a/pkg/machine/qemu/options_linux_amd64.go b/pkg/machine/qemu/options_linux_amd64.go new file mode 100644 index 000000000..cc0a4bab2 --- /dev/null +++ b/pkg/machine/qemu/options_linux_amd64.go @@ -0,0 +1,18 @@ +package qemu + +var ( + QemuCommand = "qemu-kvm" +) + +func (v *MachineVM) addArchOptions() []string { + opts := []string{"-cpu", "host"} + return opts +} + +func (v *MachineVM) prepare() error { + return nil +} + +func (v *MachineVM) archRemovalFiles() []string { + return []string{} +} diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go index d5d779c8f..678e36a70 100644 --- a/pkg/specgen/generate/ports.go +++ b/pkg/specgen/generate/ports.go @@ -6,6 +6,8 @@ import ( "strconv" "strings" + "github.com/containers/podman/v3/utils" + "github.com/containers/podman/v3/libpod/image" "github.com/containers/podman/v3/pkg/specgen" "github.com/cri-o/ocicni/pkg/ocicni" @@ -218,7 +220,7 @@ func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping, // Only get a random candidate for single entries or the start // of a range. Otherwise we just increment the candidate. if !tmp.isInRange || tmp.startOfRange { - candidate, err = specgen.GetRandomPort() + candidate, err = utils.GetRandomPort() if err != nil { return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort) } @@ -344,7 +346,7 @@ func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, img *imag for hostPort == 0 && tries > 0 { // We can't select a specific protocol, which is // unfortunate for the UDP case. - candidate, err := specgen.GetRandomPort() + candidate, err := utils.GetRandomPort() if err != nil { return nil, err } diff --git a/pkg/specgen/ports.go b/pkg/specgen/ports.go deleted file mode 100644 index 940b2a564..000000000 --- a/pkg/specgen/ports.go +++ /dev/null @@ -1,26 +0,0 @@ -package specgen - -import ( - "net" - "strconv" - - "github.com/pkg/errors" -) - -// Find a random, open port on the host -func GetRandomPort() (int, error) { - l, err := net.Listen("tcp", ":0") - if err != nil { - return 0, errors.Wrapf(err, "unable to get free TCP port") - } - defer l.Close() - _, randomPort, err := net.SplitHostPort(l.Addr().String()) - if err != nil { - return 0, errors.Wrapf(err, "unable to determine free port") - } - rp, err := strconv.Atoi(randomPort) - if err != nil { - return 0, errors.Wrapf(err, "unable to convert random port to int") - } - return rp, nil -} diff --git a/utils/ports.go b/utils/ports.go new file mode 100644 index 000000000..0a4f67dcc --- /dev/null +++ b/utils/ports.go @@ -0,0 +1,26 @@ +package utils + +import ( + "net" + "strconv" + + "github.com/pkg/errors" +) + +// Find a random, open port on the host +func GetRandomPort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, errors.Wrapf(err, "unable to get free TCP port") + } + defer l.Close() + _, randomPort, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return 0, errors.Wrapf(err, "unable to determine free port") + } + rp, err := strconv.Atoi(randomPort) + if err != nil { + return 0, errors.Wrapf(err, "unable to convert random port to int") + } + return rp, nil +} -- cgit v1.2.3-54-g00ecf