diff options
Diffstat (limited to 'pkg')
34 files changed, 1749 insertions, 59 deletions
diff --git a/pkg/api/handlers/compat/images_prune.go b/pkg/api/handlers/compat/images_prune.go index 63daaa780..ddf559ec6 100644 --- a/pkg/api/handlers/compat/images_prune.go +++ b/pkg/api/handlers/compat/images_prune.go @@ -8,8 +8,8 @@ import ( "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/api/handlers/utils" + "github.com/containers/podman/v3/pkg/util" "github.com/docker/docker/api/types" - "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -17,27 +17,20 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { var ( filters []string ) - decoder := r.Context().Value("decoder").(*schema.Decoder) runtime := r.Context().Value("runtime").(*libpod.Runtime) - query := struct { - All bool - Filters map[string][]string `schema:"filters"` - }{ - // This is where you can override the golang default value for one of fields - } - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + filterMap, err := util.PrepareFilters(r) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) return } - for k, v := range query.Filters { + for k, v := range *filterMap { for _, val := range v { filters = append(filters, fmt.Sprintf("%s=%s", k, val)) } } - imagePruneReports, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, filters) + imagePruneReports, err := runtime.ImageRuntime().PruneImages(r.Context(), false, filters) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/compat/resize.go b/pkg/api/handlers/compat/resize.go index 1bf7ad460..23ed33a22 100644 --- a/pkg/api/handlers/compat/resize.go +++ b/pkg/api/handlers/compat/resize.go @@ -19,8 +19,9 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) { // /containers/{id}/resize query := struct { - Height uint16 `schema:"h"` - Width uint16 `schema:"w"` + Height uint16 `schema:"h"` + Width uint16 `schema:"w"` + IgnoreNotRunning bool `schema:"running"` }{ // override any golang type defaults } @@ -48,14 +49,17 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) { if state, err := ctnr.State(); err != nil { utils.InternalServerError(w, errors.Wrapf(err, "cannot obtain container state")) return - } else if state != define.ContainerStateRunning { + } else if state != define.ContainerStateRunning && !query.IgnoreNotRunning { utils.Error(w, "Container not running", http.StatusConflict, fmt.Errorf("container %q in wrong state %q", name, state.String())) return } + // If container is not running, ignore since this can be a race condition, and is expected if err := ctnr.AttachResize(sz); err != nil { - utils.InternalServerError(w, errors.Wrapf(err, "cannot resize container")) - return + if errors.Cause(err) != define.ErrCtrStateInvalid || !query.IgnoreNotRunning { + utils.InternalServerError(w, errors.Wrapf(err, "cannot resize container")) + return + } } // This is not a 204, even though we write nothing, for compatibility // reasons. @@ -70,14 +74,16 @@ func ResizeTTY(w http.ResponseWriter, r *http.Request) { if state, err := ctnr.State(); err != nil { utils.InternalServerError(w, errors.Wrapf(err, "cannot obtain session container state")) return - } else if state != define.ContainerStateRunning { + } else if state != define.ContainerStateRunning && !query.IgnoreNotRunning { utils.Error(w, "Container not running", http.StatusConflict, fmt.Errorf("container %q in wrong state %q", name, state.String())) return } if err := ctnr.ExecResize(name, sz); err != nil { - utils.InternalServerError(w, errors.Wrapf(err, "cannot resize session")) - return + if errors.Cause(err) != define.ErrCtrStateInvalid || !query.IgnoreNotRunning { + utils.InternalServerError(w, errors.Wrapf(err, "cannot resize session")) + return + } } // This is not a 204, even though we write nothing, for compatibility // reasons. diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 1f306a533..158babcdc 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -22,6 +22,7 @@ import ( "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/infra/abi" "github.com/containers/podman/v3/pkg/errorhandling" + "github.com/containers/podman/v3/pkg/util" utils2 "github.com/containers/podman/v3/utils" "github.com/gorilla/schema" "github.com/pkg/errors" @@ -125,31 +126,32 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - All bool `schema:"all"` - Filters map[string][]string `schema:"filters"` + All bool `schema:"all"` }{ // override any golang type defaults } - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + filterMap, err := util.PrepareFilters(r) + + if dErr := decoder.Decode(&query, r.URL.Query()); dErr != nil || err != nil { + utils.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) return } var libpodFilters = []string{} if _, found := r.URL.Query()["filters"]; found { - dangling := query.Filters["all"] + dangling := (*filterMap)["all"] if len(dangling) > 0 { - query.All, err = strconv.ParseBool(query.Filters["all"][0]) + query.All, err = strconv.ParseBool((*filterMap)["all"][0]) if err != nil { utils.InternalServerError(w, err) return } } // dangling is special and not implemented in the libpod side of things - delete(query.Filters, "dangling") - for k, v := range query.Filters { + delete(*filterMap, "dangling") + for k, v := range *filterMap { libpodFilters = append(libpodFilters, fmt.Sprintf("%s=%s", k, v[0])) } } diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go index 743629db8..da3c9e985 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -11,6 +11,7 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/image" + "github.com/containers/podman/v3/pkg/util" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -58,13 +59,17 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { runtime := r.Context().Value("runtime").(*libpod.Runtime) query := struct { All bool - Filters map[string][]string `schema:"filters"` Digests bool Filter string // Docker 1.24 compatibility }{ // This is where you can override the golang default value for one of fields } + filterMap, err := util.PrepareFilters(r) + if err != nil { + return nil, err + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { return nil, err } @@ -72,12 +77,9 @@ func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) { if _, found := r.URL.Query()["digests"]; found && query.Digests { UnSupportedParameter("digests") } - var ( - images []*image.Image - err error - ) + var images []*image.Image - queryFilters := query.Filters + queryFilters := *filterMap if !IsLibpodRequest(r) && len(query.Filter) > 0 { // Docker 1.24 compatibility if queryFilters == nil { queryFilters = make(map[string][]string) diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 31196aa9e..b379d52ce 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -587,6 +587,11 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // type: integer // required: false // description: Width to set for the terminal, in characters + // - in: query + // name: running + // type: boolean + // required: false + // description: Ignore containers not running errors // produces: // - application/json // responses: diff --git a/pkg/api/server/register_exec.go b/pkg/api/server/register_exec.go index 0f8c827c8..de437ab1a 100644 --- a/pkg/api/server/register_exec.go +++ b/pkg/api/server/register_exec.go @@ -136,6 +136,11 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error { // name: w // type: integer // description: Width of the TTY session in characters + // - in: query + // name: running + // type: boolean + // required: false + // description: Ignore containers not running errors // produces: // - application/json // responses: diff --git a/pkg/bindings/containers/attach.go b/pkg/bindings/containers/attach.go index f48b99a95..fd8a7011d 100644 --- a/pkg/bindings/containers/attach.go +++ b/pkg/bindings/containers/attach.go @@ -307,6 +307,7 @@ func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) er if width != nil { params.Set("w", strconv.Itoa(*width)) } + params.Set("running", "true") rsp, err := conn.DoRequest(nil, http.MethodPost, endpoint, params, nil) if err != nil { return err @@ -336,7 +337,7 @@ func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, i case <-winCtx.Done(): return case <-winChange: - h, w, err := terminal.GetSize(int(file.Fd())) + w, h, err := terminal.GetSize(int(file.Fd())) if err != nil { logrus.Warnf("failed to obtain TTY size: %v", err) } diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index 2d0e65bb4..f63e35bf1 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -210,8 +210,9 @@ type RenameOptions struct { // ResizeTTYOptions are optional options for resizing // container TTYs type ResizeTTYOptions struct { - Height *int - Width *int + Height *int + Width *int + Running *bool } //go:generate go run ../generator/generator.go ResizeExecTTYOptions diff --git a/pkg/bindings/containers/types_resizetty_options.go b/pkg/bindings/containers/types_resizetty_options.go index 68527b330..94946692f 100644 --- a/pkg/bindings/containers/types_resizetty_options.go +++ b/pkg/bindings/containers/types_resizetty_options.go @@ -51,3 +51,19 @@ func (o *ResizeTTYOptions) GetWidth() int { } return *o.Width } + +// WithRunning +func (o *ResizeTTYOptions) WithRunning(value bool) *ResizeTTYOptions { + v := &value + o.Running = v + return o +} + +// GetRunning +func (o *ResizeTTYOptions) GetRunning() bool { + var running bool + if o.Running == nil { + return running + } + return *o.Running +} diff --git a/pkg/machine/config.go b/pkg/machine/config.go new file mode 100644 index 000000000..4933deee8 --- /dev/null +++ b/pkg/machine/config.go @@ -0,0 +1,120 @@ +package machine + +import ( + "net" + "net/url" + "os" + "path/filepath" + + "github.com/containers/storage/pkg/homedir" +) + +type InitOptions 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 { + Execute bool + 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..0c6a2485e --- /dev/null +++ b/pkg/machine/fcos.go @@ -0,0 +1,160 @@ +package machine + +import ( + "crypto/sha256" + "io" + "io/ioutil" + url2 "net/url" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/containers/storage/pkg/archive" + digest "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" +) + +// 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..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..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..b97eb991a --- /dev/null +++ b/pkg/machine/qemu/machine.go @@ -0,0 +1,325 @@ +package qemu + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "time" + + "github.com/containers/podman/v3/utils" + + "github.com/containers/podman/v3/pkg/machine" + "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 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 + } + 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 := 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,nowait"}...) + + // Add network + cmd = append(cmd, "-nic", "user,model=virtio,hostfwd=tcp::"+strconv.Itoa(vm.Port)+"-:22") + + 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) { + // 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 +} + +// Init writes the json configuration file to the filesystem for +// other verbs (start, stop) +func (v *MachineVM) Init(opts machine.InitOptions) 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 + 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) + 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 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", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} 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 6cf83ed81..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 = 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 := getRandomPort() + candidate, err := utils.GetRandomPort() if err != nil { return nil, err } @@ -419,21 +421,3 @@ func checkProtocol(protocol string, allowSCTP bool) ([]string, error) { return finalProto, nil } - -// 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/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index fb7d65da4..f665fc0be 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -54,7 +54,7 @@ const ( // Namespace describes the namespace type Namespace struct { NSMode NamespaceMode `json:"nsmode,omitempty"` - Value string `json:"string,omitempty"` + Value string `json:"value,omitempty"` } // IsDefault returns whether the namespace is set to the default setting (which diff --git a/pkg/systemd/generate/common.go b/pkg/systemd/generate/common.go index 94a6f4cb5..19d468403 100644 --- a/pkg/systemd/generate/common.go +++ b/pkg/systemd/generate/common.go @@ -36,6 +36,7 @@ Description=Podman {{{{.ServiceName}}}}.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor={{{{.GraphRoot}}}} {{{{.RunRoot}}}} ` // filterPodFlags removes --pod and --pod-id-file from the specified command. diff --git a/pkg/systemd/generate/containers.go b/pkg/systemd/generate/containers.go index 9343a5067..bc13a6116 100644 --- a/pkg/systemd/generate/containers.go +++ b/pkg/systemd/generate/containers.go @@ -71,6 +71,12 @@ type containerInfo struct { // If not nil, the container is part of the pod. We can use the // podInfo to extract the relevant data. Pod *podInfo + // Location of the GraphRoot for the container. Required for ensuring the + // volume has finished mounting when coming online at boot. + GraphRoot string + // Location of the RunRoot for the container. Required for ensuring the tmpfs + // or volume exists and is mounted when coming online at boot. + RunRoot string } const containerTemplate = headerTemplate + ` @@ -132,6 +138,21 @@ func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSyste nameOrID, serviceName := containerServiceName(ctr, options) + store := ctr.Runtime().GetStore() + if store == nil { + return nil, errors.Errorf("could not determine storage store for container") + } + + graphRoot := store.GraphRoot() + if graphRoot == "" { + return nil, errors.Errorf("could not lookup container's graphroot: got empty string") + } + + runRoot := store.RunRoot() + if runRoot == "" { + return nil, errors.Errorf("could not lookup container's runroot: got empty string") + } + info := containerInfo{ ServiceName: serviceName, ContainerNameOrID: nameOrID, @@ -140,6 +161,8 @@ func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSyste StopTimeout: timeout, GenerateTimestamp: true, CreateCommand: createCommand, + GraphRoot: graphRoot, + RunRoot: runRoot, } return &info, nil diff --git a/pkg/systemd/generate/containers_test.go b/pkg/systemd/generate/containers_test.go index ebbbdb786..1359c1a37 100644 --- a/pkg/systemd/generate/containers_test.go +++ b/pkg/systemd/generate/containers_test.go @@ -48,6 +48,7 @@ Description=Podman container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -73,6 +74,7 @@ Description=Podman container-foobar.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -96,6 +98,7 @@ Description=Podman container-foobar.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage BindsTo=a.service b.service c.service pod.service After=a.service b.service c.service pod.service @@ -121,6 +124,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -145,6 +149,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -169,6 +174,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -193,6 +199,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -217,6 +224,7 @@ Description=Podman container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -242,6 +250,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -270,6 +279,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -294,6 +304,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -318,6 +329,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -342,6 +354,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -366,6 +379,7 @@ Description=Podman jadda-jadda.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage [Service] Environment=PODMAN_SYSTEMD_UNIT=%n @@ -400,6 +414,8 @@ WantedBy=multi-user.target default.target StopTimeout: 22, PodmanVersion: "CI", EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodID, false, @@ -416,6 +432,8 @@ WantedBy=multi-user.target default.target StopTimeout: 22, PodmanVersion: "CI", EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodIDNoHeaderInfo, false, @@ -432,6 +450,8 @@ WantedBy=multi-user.target default.target StopTimeout: 10, PodmanVersion: "CI", EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodName, false, @@ -449,6 +469,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", BoundToServices: []string{"pod", "a", "b", "c"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNameBoundTo, false, @@ -464,6 +486,8 @@ WantedBy=multi-user.target default.target StopTimeout: 10, PodmanVersion: "CI", EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, "", false, @@ -481,6 +505,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "container", "run", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN", "foo=arg \"with \" space"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodWithNameAndGeneric, true, @@ -498,6 +524,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "-d", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodWithExplicitShortDetachParam, true, @@ -515,6 +543,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "-d", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", Pod: &podInfo{ PodIDFile: "%t/pod-foobar.pod-id-file", }, @@ -535,6 +565,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "--detach", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNameNewDetach, true, @@ -552,6 +584,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodIDNew, true, @@ -569,6 +603,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "--detach=true", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, genGoodNewDetach("--detach=true"), true, @@ -586,6 +622,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "--detach=false", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, genGoodNewDetach("-d"), true, @@ -603,6 +641,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "--name", "test", "-p", "80:80", "--detach=false", "awesome-image:latest", "somecmd", "--detach=false"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNameNewDetachFalseWithCmd, true, @@ -620,6 +660,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "--name", "test", "-p", "80:80", "--detach=false", "--detach=false", "awesome-image:latest", "somecmd", "--detach=false"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNameNewDetachFalseWithCmd, true, @@ -637,6 +679,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "-dti", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, genGoodNewDetach("-dti"), true, @@ -654,6 +698,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "run", "-tid", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, genGoodNewDetach("-tid"), true, @@ -671,6 +717,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "--events-backend", "none", "--runroot", "/root", "run", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNewRootFlags, true, @@ -688,6 +736,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "container", "create", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodContainerCreate, true, @@ -705,6 +755,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "create", "--name", "test", "--log-driver=journald", "--log-opt=tag={{.Name}}", "awesome-image:latest"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNewWithJournaldTag, true, @@ -722,6 +774,8 @@ WantedBy=multi-user.target default.target PodmanVersion: "CI", CreateCommand: []string{"I'll get stripped", "create", "--name", "test", "awesome-image:latest", "sh", "-c", "kill $$ && echo %\\"}, EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", }, goodNewWithSpecialChars, true, diff --git a/pkg/systemd/generate/pods.go b/pkg/systemd/generate/pods.go index f96058d36..a76979ecf 100644 --- a/pkg/systemd/generate/pods.go +++ b/pkg/systemd/generate/pods.go @@ -73,6 +73,12 @@ type podInfo struct { ExecStopPost string // Removes autogenerated by Podman and timestamp if set to true GenerateNoHeader bool + // Location of the GraphRoot for the pod. Required for ensuring the + // volume has finished mounting when coming online at boot. + GraphRoot string + // Location of the RunRoot for the pod. Required for ensuring the tmpfs + // or volume exists and is mounted when coming online at boot. + RunRoot string } const podTemplate = headerTemplate + `Requires={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} diff --git a/pkg/systemd/generate/pods_test.go b/pkg/systemd/generate/pods_test.go index 50c8d4556..559f7365f 100644 --- a/pkg/systemd/generate/pods_test.go +++ b/pkg/systemd/generate/pods_test.go @@ -47,6 +47,7 @@ Description=Podman pod-123abc.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage Requires=container-1.service container-2.service Before=container-1.service container-2.service @@ -74,6 +75,7 @@ Description=Podman pod-123abc.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage Requires=container-1.service container-2.service Before=container-1.service container-2.service @@ -101,6 +103,7 @@ Description=Podman pod-123abc.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage Requires=container-1.service container-2.service Before=container-1.service container-2.service @@ -128,6 +131,7 @@ Description=Podman pod-123abc.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage Requires=container-1.service container-2.service Before=container-1.service container-2.service @@ -155,6 +159,7 @@ Description=Podman pod-123abc.service Documentation=man:podman-generate-systemd(1) Wants=network.target After=network-online.target +RequiresMountsFor=/var/lib/containers/storage /var/run/containers/storage Requires=container-1.service container-2.service Before=container-1.service container-2.service @@ -191,6 +196,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 42, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "pod", "create", "--name", "foo", "bar=arg with space"}, }, @@ -208,6 +215,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 42, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "pod", "create", "--name", "foo", "bar=arg with space"}, }, @@ -225,6 +234,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 42, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "--events-backend", "none", "--runroot", "/root", "pod", "create", "--name", "foo", "bar=arg with space"}, }, @@ -242,6 +253,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 10, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "pod", "create", "--name", "foo", "bar=arg with space"}, }, @@ -259,6 +272,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 10, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "--events-backend", "none", "--runroot", "/root", "pod", "create", "--name", "foo", "bar=arg with space"}, }, @@ -276,6 +291,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 10, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "pod", "create", "--name", "foo", "--replace=false"}, }, @@ -293,6 +310,8 @@ WantedBy=multi-user.target default.target PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", StopTimeout: 10, PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", RequiredServices: []string{"container-1", "container-2"}, CreateCommand: []string{"podman", "pod", "create", "--name", "foo", "--label", "key={{someval}}"}, }, |