summaryrefslogtreecommitdiff
path: root/pkg/machine
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/machine')
-rw-r--r--pkg/machine/config.go132
-rw-r--r--pkg/machine/connection.go50
-rw-r--r--pkg/machine/fcos.go122
-rw-r--r--pkg/machine/fcos_amd64.go68
-rw-r--r--pkg/machine/fcos_arm64.go177
-rw-r--r--pkg/machine/ignition.go180
-rw-r--r--pkg/machine/ignition_schema.go251
-rw-r--r--pkg/machine/keys.go25
-rw-r--r--pkg/machine/libvirt/config.go4
-rw-r--r--pkg/machine/libvirt/machine.go15
-rw-r--r--pkg/machine/pull.go210
-rw-r--r--pkg/machine/qemu/config.go43
-rw-r--r--pkg/machine/qemu/machine.go512
-rw-r--r--pkg/machine/qemu/options_darwin.go15
-rw-r--r--pkg/machine/qemu/options_darwin_amd64.go18
-rw-r--r--pkg/machine/qemu/options_darwin_arm64.go36
-rw-r--r--pkg/machine/qemu/options_linux.go7
-rw-r--r--pkg/machine/qemu/options_linux_amd64.go21
-rw-r--r--pkg/machine/qemu/options_linux_arm64.go41
19 files changed, 1927 insertions, 0 deletions
diff --git a/pkg/machine/config.go b/pkg/machine/config.go
new file mode 100644
index 000000000..32b3b5c2b
--- /dev/null
+++ b/pkg/machine/config.go
@@ -0,0 +1,132 @@
+package machine
+
+import (
+ "net"
+ "net/url"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/containers/storage/pkg/homedir"
+ "github.com/pkg/errors"
+)
+
+type InitOptions struct {
+ CPUS uint64
+ DiskSize uint64
+ IgnitionPath string
+ ImagePath string
+ IsDefault bool
+ Memory uint64
+ Name string
+ URI url.URL
+ Username string
+}
+
+type RemoteConnectionType string
+
+var (
+ SSHRemoteConnection RemoteConnectionType = "ssh"
+ DefaultIgnitionUserName = "core"
+ ErrNoSuchVM = errors.New("VM does not exist")
+ ErrVMAlreadyExists = errors.New("VM already exists")
+)
+
+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 ListOptions struct{}
+
+type ListResponse struct {
+ Name string
+ CreatedAt time.Time
+ LastUp time.Time
+ Running bool
+ VMType string
+}
+
+type SSHOptions struct {
+ Args []string
+}
+type StartOptions struct{}
+
+type StopOptions struct{}
+
+type RemoveOptions struct {
+ Force bool
+ SaveKeys bool
+ SaveImage bool
+ SaveIgnition bool
+}
+
+type VM interface {
+ Init(opts InitOptions) 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
+}
+
+type DistributionDownload interface {
+ DownloadImage() error
+ Get() *Download
+}
+
+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: "",
+ }
+ 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..32f943c87
--- /dev/null
+++ b/pkg/machine/fcos.go
@@ -0,0 +1,122 @@
+package machine
+
+import (
+ "crypto/sha256"
+ "io/ioutil"
+ url2 "net/url"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ 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
+ }
+ }
+ return Decompress(f.LocalPath, f.getLocalUncompressedName())
+}
+
+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..f5cd5a505
--- /dev/null
+++ b/pkg/machine/fcos_arm64.go
@@ -0,0 +1,177 @@
+package machine
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ url2 "net/url"
+
+ "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{}
+ 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
+ }
+ pathURL, err := url2.Parse(meta.BuildArtifacts.Qemu.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ baseURL, err := url2.Parse(aarchBaseURL)
+ if err != nil {
+ return nil, err
+ }
+ pullURL := baseURL.ResolveReference(pathURL)
+ return &fcosDownloadInfo{
+ Location: pullURL.String(),
+ 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..a68d68ac3
--- /dev/null
+++ b/pkg/machine/ignition.go
@@ -0,0 +1,180 @@
+package machine
+
+import (
+ "encoding/json"
+ "fmt"
+ "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}
+}
+
+type DynamicIgnition struct {
+ Name string
+ Key string
+ VMName string
+ WritePath string
+}
+
+// NewIgnitionFile
+func NewIgnitionFile(ign DynamicIgnition) error {
+ if len(ign.Name) < 1 {
+ ign.Name = DefaultIgnitionUserName
+ }
+ ignVersion := Ignition{
+ Version: "3.2.0",
+ }
+
+ ignPassword := Passwd{
+ Users: []PasswdUser{{
+ Name: ign.Name,
+ SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)},
+ }},
+ }
+
+ ignStorage := Storage{
+ Directories: getDirs(ign.Name),
+ Files: getFiles(ign.Name),
+ Links: getLinks(ign.Name),
+ }
+
+ // ready is a unit file that sets up the virtual serial device
+ // where when the VM is done configuring, it will send an ack
+ // so a listening host knows it can being interacting with it
+ ready := `[Unit]
+Requires=dev-virtio\\x2dports-%s.device
+OnFailure=emergency.target
+OnFailureJobMode=isolate
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/sh -c '/usr/bin/echo Ready >/dev/%s'
+[Install]
+RequiredBy=multi-user.target
+`
+ _ = ready
+ ignSystemd := Systemd{
+ Units: []Unit{
+ {
+ Enabled: boolToPtr(true),
+ Name: "podman.socket",
+ },
+ {
+ Enabled: boolToPtr(true),
+ Name: "ready.service",
+ Contents: strToPtr(fmt.Sprintf(ready, "vport1p1", "vport1p1")),
+ },
+ }}
+ ignConfig := Config{
+ Ignition: ignVersion,
+ Passwd: ignPassword,
+ Storage: ignStorage,
+ Systemd: ignSystemd,
+ }
+ b, err := json.Marshal(ignConfig)
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(ign.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..c38f63853
--- /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) Init(name string, opts machine.InitOptions) 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..41abe6993
--- /dev/null
+++ b/pkg/machine/pull.go
@@ -0,0 +1,210 @@
+package machine
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ url2 "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/containers/image/v5/pkg/compression"
+ "github.com/docker/docker/pkg/archive"
+ "github.com/sirupsen/logrus"
+ "github.com/vbauerster/mpb/v6"
+ "github.com/vbauerster/mpb/v6/decor"
+)
+
+// GenericDownload is used when a user provides a URL
+// or path for an image
+type GenericDownload struct {
+ Download
+}
+
+// NewGenericDownloader is used when the disk image is provided by the user
+func NewGenericDownloader(vmType, vmName, pullPath string) (DistributionDownload, error) {
+ var (
+ imageName string
+ )
+ dataDir, err := GetDataDir(vmType)
+ if err != nil {
+ return nil, err
+ }
+ dl := Download{}
+ // Is pullpath a file or url?
+ getURL, err := url2.Parse(pullPath)
+ if err != nil {
+ return nil, err
+ }
+ if len(getURL.Scheme) > 0 {
+ urlSplit := strings.Split(pullPath, "/")
+ imageName = urlSplit[len(urlSplit)-1]
+ dl.LocalUncompressedFile = filepath.Join(dataDir, imageName)
+ dl.URL = getURL
+ dl.LocalPath = filepath.Join(dataDir, imageName)
+ } else {
+ // Dealing with FilePath
+ imageName = filepath.Base(pullPath)
+ dl.LocalUncompressedFile = filepath.Join(dataDir, imageName)
+ dl.LocalPath = pullPath
+ }
+ dl.VMName = vmName
+ dl.ImageName = imageName
+ // The download needs to be pulled into the datadir
+
+ gd := GenericDownload{Download: dl}
+ gd.LocalUncompressedFile = gd.getLocalUncompressedName()
+ return gd, nil
+}
+
+func (g GenericDownload) getLocalUncompressedName() string {
+ var (
+ extension string
+ )
+ switch {
+ case strings.HasSuffix(g.LocalPath, ".bz2"):
+ extension = ".bz2"
+ case strings.HasSuffix(g.LocalPath, ".gz"):
+ extension = ".gz"
+ case strings.HasSuffix(g.LocalPath, ".xz"):
+ extension = ".xz"
+ }
+ uncompressedFilename := filepath.Join(filepath.Dir(g.LocalUncompressedFile), g.VMName+"_"+g.ImageName)
+ return strings.TrimSuffix(uncompressedFilename, extension)
+}
+
+func (g GenericDownload) DownloadImage() error {
+ // If we have a URL for this "downloader", we now pull it
+ if g.URL != nil {
+ if err := DownloadVMImage(g.URL, g.LocalPath); err != nil {
+ return err
+ }
+ }
+ return Decompress(g.LocalPath, g.getLocalUncompressedName())
+}
+
+func (g GenericDownload) Get() *Download {
+ return &g.Download
+}
+
+// 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: %s", downloadURL, 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
+}
+
+func Decompress(localPath, uncompressedPath string) error {
+ uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
+ if err != nil {
+ return err
+ }
+ sourceFile, err := ioutil.ReadFile(localPath)
+ if err != nil {
+ return err
+ }
+
+ if compressionType := archive.DetectCompression(sourceFile); compressionType.Extension() == "tar.xz" {
+ return decompressXZ(localPath, uncompressedFileWriter)
+ }
+ return decompressEverythingElse(localPath, uncompressedFileWriter)
+}
+
+// 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()
+}
+
+func decompressEverythingElse(src string, output io.Writer) error {
+ fmt.Println("Extracting compressed file")
+ f, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ uncompressStream, _, err := compression.AutoDecompress(f)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := uncompressStream.Close(); err != nil {
+ logrus.Error(err)
+ }
+ }()
+
+ _, err = io.Copy(output, uncompressStream)
+ return err
+}
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..2652ebc10
--- /dev/null
+++ b/pkg/machine/qemu/machine.go
@@ -0,0 +1,512 @@
+package qemu
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/containers/podman/v3/pkg/machine"
+ "github.com/containers/podman/v3/utils"
+ "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"
+)
+
+// NewMachine initializes an instance of a virtual machine based on the qemu
+// virtualization.
+func NewMachine(opts machine.InitOptions) (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
+ }
+ 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 := utils.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
+ cmd = append(cmd, []string{"-smp", strconv.Itoa(int(vm.CPUs))}...)
+ // 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=on,wait=off"}...)
+
+ // Add network
+ cmd = append(cmd, "-nic", "user,model=virtio,hostfwd=tcp::"+strconv.Itoa(vm.Port)+"-:22")
+
+ socketPath, err := getSocketDir()
+ if err != nil {
+ return nil, err
+ }
+ virtualSocketPath := filepath.Join(socketPath, "podman", vm.Name+"_ready.sock")
+ // Add serial port for readiness
+ cmd = append(cmd, []string{
+ "-device", "virtio-serial",
+ "-chardev", "socket,path=" + virtualSocketPath + ",server=on,wait=off,id=" + vm.Name + "_ready",
+ "-device", "virtserialport,chardev=" + vm.Name + "_ready" + ",name=org.fedoraproject.port.0"}...)
+ vm.CmdLine = cmd
+ 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) {
+ vm := new(MachineVM)
+ vmConfigDir, err := machine.GetConfDir(vmtype)
+ if err != nil {
+ return nil, err
+ }
+ b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json"))
+ if os.IsNotExist(err) {
+ return nil, errors.Wrap(machine.ErrNoSuchVM, name)
+ }
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(b, vm)
+ logrus.Debug(vm.CmdLine)
+ return vm, err
+}
+
+// Init writes the json configuration file to the filesystem for
+// other verbs (start, stop)
+func (v *MachineVM) Init(opts machine.InitOptions) error {
+ var (
+ key string
+ )
+ 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)
+
+ // The user has provided an alternate image which can be a file path
+ // or URL.
+ if len(opts.ImagePath) > 0 {
+ g, err := machine.NewGenericDownloader(vmtype, v.Name, opts.ImagePath)
+ if err != nil {
+ return err
+ }
+ v.ImagePath = g.Get().LocalUncompressedFile
+ if err := g.DownloadImage(); err != nil {
+ return err
+ }
+ } else {
+ // Get the image as usual
+ 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
+ if len(opts.IgnitionPath) < 1 {
+ 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
+ }
+ } else {
+ fmt.Println("An ignition path was provided. No SSH connection was added to Podman")
+ }
+ // 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
+ }
+
+ // User has provided ignition file so keygen
+ // will be skipped.
+ if len(opts.IgnitionPath) < 1 {
+ 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
+ }
+
+ originalDiskSize, err := getDiskSize(v.ImagePath)
+ if err != nil {
+ return err
+ }
+ // Resize the disk image to input disk size
+ // only if the virtualdisk size is less than
+ // the given disk size
+ if opts.DiskSize<<(10*3) > originalDiskSize {
+ resize := exec.Command("qemu-img", []string{"resize", v.ImagePath, strconv.Itoa(int(opts.DiskSize)) + "G"}...)
+ resize.Stdout = os.Stdout
+ resize.Stderr = os.Stderr
+ if err := resize.Run(); err != nil {
+ return errors.Errorf("error resizing image: %q", err)
+ }
+ }
+ // If the user provides an ignition file, we need to
+ // copy it into the conf dir
+ if len(opts.IgnitionPath) > 0 {
+ inputIgnition, err := ioutil.ReadFile(opts.IgnitionPath)
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(v.IgnitionFilePath, inputIgnition, 0644)
+ }
+ // Write the ignition file
+ ign := machine.DynamicIgnition{
+ Name: opts.Username,
+ Key: key,
+ VMName: v.Name,
+ WritePath: v.IgnitionFilePath,
+ }
+ return machine.NewIgnitionFile(ign)
+}
+
+// Start executes the qemu command line and forks it
+func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
+ var (
+ conn net.Conn
+ err error
+ wait time.Duration = time.Millisecond * 500
+ )
+ attr := new(os.ProcAttr)
+ files := []*os.File{os.Stdin, os.Stdout, os.Stderr}
+ attr.Files = files
+ logrus.Debug(v.CmdLine)
+ cmd := v.CmdLine
+
+ // 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 {
+ cmd = append(cmd, "-display", "none")
+ }
+
+ _, err = os.StartProcess(v.CmdLine[0], cmd, attr)
+ if err != nil {
+ return err
+ }
+ fmt.Println("Waiting for VM ...")
+ socketPath, err := getSocketDir()
+ if err != nil {
+ return err
+ }
+
+ // The socket is not made until the qemu process is running so here
+ // we do a backoff waiting for it. Once we have a conn, we break and
+ // then wait to read it.
+ for i := 0; i < 6; i++ {
+ conn, err = net.Dial("unix", filepath.Join(socketPath, "podman", v.Name+"_ready.sock"))
+ if err == nil {
+ break
+ }
+ time.Sleep(wait)
+ wait++
+ }
+ if err != nil {
+ return err
+ }
+ _, err = bufio.NewReader(conn).ReadString('\n')
+ 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 := 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, "qmp_"+name+".sock"),
+ Timeout: timeout,
+ }
+ return monitor, nil
+}
+
+func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (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)
+
+ args := []string{"-i", v.IdentityPath, "-p", port, sshDestination}
+ if len(opts.Args) > 0 {
+ 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", args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Stdin = os.Stdin
+
+ return cmd.Run()
+}
+
+// executes qemu-image info to get the virtual disk size
+// of the diskimage
+func getDiskSize(path string) (uint64, error) {
+ diskInfo := exec.Command("qemu-img", "info", "--output", "json", path)
+ stdout, err := diskInfo.StdoutPipe()
+ if err != nil {
+ return 0, err
+ }
+ if err := diskInfo.Start(); err != nil {
+ return 0, err
+ }
+ tmpInfo := struct {
+ VirtualSize uint64 `json:"virtual-size"`
+ Filename string `json:"filename"`
+ ClusterSize int64 `json:"cluster-size"`
+ Format string `json:"format"`
+ FormatSpecific struct {
+ Type string `json:"type"`
+ Data map[string]string `json:"data"`
+ }
+ DirtyFlag bool `json:"dirty-flag"`
+ }{}
+ if err := json.NewDecoder(stdout).Decode(&tmpInfo); err != nil {
+ return 0, err
+ }
+ if err := diskInfo.Wait(); err != nil {
+ return 0, err
+ }
+ return tmpInfo.VirtualSize, nil
+}
+
+// List lists all vm's that use qemu virtualization
+func List(_ machine.ListOptions) ([]*machine.ListResponse, error) {
+ return GetVMInfos()
+}
+
+func GetVMInfos() ([]*machine.ListResponse, error) {
+ vmConfigDir, err := machine.GetConfDir(vmtype)
+ if err != nil {
+ return nil, err
+ }
+
+ var listed []*machine.ListResponse
+
+ if err = filepath.Walk(vmConfigDir, func(path string, info os.FileInfo, err error) error {
+ vm := new(MachineVM)
+ if strings.HasSuffix(info.Name(), ".json") {
+ fullPath := filepath.Join(vmConfigDir, info.Name())
+ b, err := ioutil.ReadFile(fullPath)
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(b, vm)
+ if err != nil {
+ return err
+ }
+ listEntry := new(machine.ListResponse)
+
+ listEntry.Name = vm.Name
+ listEntry.VMType = "qemu"
+ fi, err := os.Stat(fullPath)
+ if err != nil {
+ return err
+ }
+ listEntry.CreatedAt = fi.ModTime()
+
+ fi, err = os.Stat(vm.ImagePath)
+ if err != nil {
+ return err
+ }
+ listEntry.LastUp = fi.ModTime()
+ if vm.isRunning() {
+ listEntry.Running = true
+ }
+
+ listed = append(listed, listEntry)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return listed, err
+}
+
+func IsValidVMName(name string) (bool, error) {
+ infos, err := GetVMInfos()
+ if err != nil {
+ return false, err
+ }
+ for _, vm := range infos {
+ if vm.Name == name {
+ return true, nil
+ }
+ }
+ return false, nil
+}
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..3edd97ea1
--- /dev/null
+++ b/pkg/machine/qemu/options_linux_amd64.go
@@ -0,0 +1,21 @@
+package qemu
+
+var (
+ QemuCommand = "qemu-system-x86_64"
+)
+
+func (v *MachineVM) addArchOptions() []string {
+ opts := []string{
+ "-accel", "kvm",
+ "-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_linux_arm64.go b/pkg/machine/qemu/options_linux_arm64.go
new file mode 100644
index 000000000..948117653
--- /dev/null
+++ b/pkg/machine/qemu/options_linux_arm64.go
@@ -0,0 +1,41 @@
+package qemu
+
+import (
+ "os"
+ "path/filepath"
+)
+
+var (
+ QemuCommand = "qemu-system-aarch64"
+)
+
+func (v *MachineVM) addArchOptions() []string {
+ opts := []string{
+ "-accel", "kvm",
+ "-cpu", "host",
+ "-M", "virt,gic-version=max",
+ "-bios", getQemuUefiFile("QEMU_EFI.fd"),
+ }
+ return opts
+}
+
+func (v *MachineVM) prepare() error {
+ return nil
+}
+
+func (v *MachineVM) archRemovalFiles() []string {
+ return []string{}
+}
+
+func getQemuUefiFile(name string) string {
+ dirs := []string{
+ "/usr/share/qemu-efi-aarch64",
+ "/usr/share/edk2/aarch64",
+ }
+ for _, dir := range dirs {
+ if _, err := os.Stat(dir); err == nil {
+ return filepath.Join(dir, name)
+ }
+ }
+ return name
+}