diff options
author | Brent Baude <bbaude@redhat.com> | 2020-03-27 08:20:13 -0500 |
---|---|---|
committer | Brent Baude <bbaude@redhat.com> | 2020-04-03 09:32:06 -0500 |
commit | 8a16674722ab4a33ce66e57d151b09ac348e8e6d (patch) | |
tree | 03505f230a58e656e8b85c44b2505696e16c50c2 /pkg | |
parent | ccb9e579c48141f70d016adfa9a9d4934bbdf976 (diff) | |
download | podman-8a16674722ab4a33ce66e57d151b09ac348e8e6d.tar.gz podman-8a16674722ab4a33ce66e57d151b09ac348e8e6d.tar.bz2 podman-8a16674722ab4a33ce66e57d151b09ac348e8e6d.zip |
podmanv2 checkpoint and restore
add the ability to checkpoint and restore containers on v2podman
Signed-off-by: Brent Baude <bbaude@redhat.com>
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/adapter/containers.go | 3 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/containers.go | 131 | ||||
-rw-r--r-- | pkg/api/server/register_containers.go | 95 | ||||
-rw-r--r-- | pkg/bindings/containers/checkpoint.go | 79 | ||||
-rw-r--r-- | pkg/checkpoint/checkpoint_restore.go (renamed from pkg/adapter/checkpoint_restore.go) | 8 | ||||
-rw-r--r-- | pkg/domain/entities/containers.go | 32 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 109 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/containers.go | 72 |
9 files changed, 524 insertions, 7 deletions
diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index c395ffc7f..b4ebeb944 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -26,6 +26,7 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/checkpoint" envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/systemd/generate" "github.com/containers/storage" @@ -625,7 +626,7 @@ func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues) switch { case c.Import != "": - containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) + containers, err = checkpoint.CRImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) case c.All: containers, err = r.GetContainers(filterFuncs...) default: diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index cdc34004f..fde72552b 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,16 +1,21 @@ package libpod import ( + "io/ioutil" "net/http" + "os" "path/filepath" "sort" "strconv" "time" + "github.com/containers/libpod/pkg/api/handlers/compat" + "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -325,3 +330,129 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts shared.P } return ps, nil } + +func Checkpoint(w http.ResponseWriter, r *http.Request) { + var targetFile string + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Keep bool `schema:"keep"` + LeaveRunning bool `schema:"leaveRunning"` + TCPEstablished bool `schema:"tcpEstablished"` + Export bool `schema:"export"` + IgnoreRootFS bool `schema:"ignoreRootFS"` + }{ + // override any golang type defaults + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if query.Export { + tmpFile, err := ioutil.TempFile("", "checkpoint") + if err != nil { + utils.InternalServerError(w, err) + return + } + defer os.Remove(tmpFile.Name()) + if err := tmpFile.Close(); err != nil { + utils.InternalServerError(w, err) + return + } + targetFile = tmpFile.Name() + } + options := libpod.ContainerCheckpointOptions{ + Keep: query.Keep, + KeepRunning: query.LeaveRunning, + TCPEstablished: query.TCPEstablished, + IgnoreRootfs: query.IgnoreRootFS, + } + if query.Export { + options.TargetFile = targetFile + } + err = ctr.Checkpoint(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + return + } + if query.Export { + f, err := os.Open(targetFile) + if err != nil { + utils.InternalServerError(w, err) + return + } + defer f.Close() + utils.WriteResponse(w, http.StatusOK, f) + return + } + utils.WriteResponse(w, http.StatusOK, entities.CheckpointReport{Id: ctr.ID()}) +} + +func Restore(w http.ResponseWriter, r *http.Request) { + var ( + targetFile string + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Keep bool `schema:"keep"` + TCPEstablished bool `schema:"tcpEstablished"` + Import bool `schema:"import"` + Name string `schema:"name"` + IgnoreRootFS bool `schema:"ignoreRootFS"` + IgnoreStaticIP bool `schema:"ignoreStaticIP"` + IgnoreStaticMAC bool `schema:"ignoreStaticMAC"` + }{ + // override any golang type defaults + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if query.Import { + t, err := ioutil.TempFile("", "restore") + if err != nil { + utils.InternalServerError(w, err) + return + } + defer t.Close() + if err := compat.SaveFromBody(t, r); err != nil { + utils.InternalServerError(w, err) + return + } + targetFile = t.Name() + } + + options := libpod.ContainerCheckpointOptions{ + Keep: query.Keep, + TCPEstablished: query.TCPEstablished, + IgnoreRootfs: query.IgnoreRootFS, + IgnoreStaticIP: query.IgnoreStaticIP, + IgnoreStaticMAC: query.IgnoreStaticMAC, + } + if query.Import { + options.TargetFile = targetFile + options.Name = query.Name + } + err = ctr.Restore(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, entities.RestoreReport{Id: ctr.ID()}) +} diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 145c054c0..f126112d0 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -1282,5 +1282,100 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/export"), s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) + // swagger:operation GET /libpod/containers/{name}/checkout libpod libpodCheckpointContainer + // --- + // tags: + // - containers + // summary: Checkpoint a container + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // - in: query + // name: keep + // type: boolean + // description: keep all temporary checkpoint files + // - in: query + // name: leaveRunning + // type: boolean + // description: leave the container running after writing checkpoint to disk + // - in: query + // name: tcpEstablished + // type: boolean + // description: checkpoint a container with established TCP connections + // - in: query + // name: export + // type: boolean + // description: export the checkpoint image to a tar.gz + // - in: query + // name: ignoreRootFS + // type: boolean + // description: do not include root file-system changes when exporting + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body if exported + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/checkpoint"), s.APIHandler(libpod.Checkpoint)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/{name} restore libpod libpodRestoreContainer + // --- + // tags: + // - containers + // summary: Restore a container + // description: Restore a container from a checkpoint. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or id of the container + // - in: query + // name: name + // type: string + // description: the name of the container when restored from a tar. can only be used with import + // - in: query + // name: keep + // type: boolean + // description: keep all temporary checkpoint files + // - in: query + // name: leaveRunning + // type: boolean + // description: leave the container running after writing checkpoint to disk + // - in: query + // name: tcpEstablished + // type: boolean + // description: checkpoint a container with established TCP connections + // - in: query + // name: import + // type: boolean + // description: import the restore from a checkpoint tar.gz + // - in: query + // name: ignoreRootFS + // type: boolean + // description: do not include root file-system changes when exporting + // - in: query + // name: ignoreStaticIP + // type: boolean + // description: ignore IP address if set statically + // - in: query + // name: ignoreStaticMAC + // type: boolean + // description: ignore MAC address if set statically + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body if exported + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/restore"), s.APIHandler(libpod.Restore)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/containers/checkpoint.go b/pkg/bindings/containers/checkpoint.go new file mode 100644 index 000000000..84924587b --- /dev/null +++ b/pkg/bindings/containers/checkpoint.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" +) + +// Checkpoint checkpoints the given container (identified by nameOrId). All additional +// options are options and allow for more fine grained control of the checkpoint process. +func Checkpoint(ctx context.Context, nameOrId string, keep, leaveRunning, tcpEstablished, ignoreRootFS *bool, export *string) (*entities.CheckpointReport, error) { + var report entities.CheckpointReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if keep != nil { + params.Set("keep", strconv.FormatBool(*keep)) + } + if leaveRunning != nil { + params.Set("leaveRunning", strconv.FormatBool(*leaveRunning)) + } + if tcpEstablished != nil { + params.Set("TCPestablished", strconv.FormatBool(*tcpEstablished)) + } + if ignoreRootFS != nil { + params.Set("ignoreRootFS", strconv.FormatBool(*ignoreRootFS)) + } + if export != nil { + params.Set("export", *export) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/checkpoint", params, nameOrId) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} + +// Restore restores a checkpointed container to running. The container is identified by the nameOrId option. All +// additional options are optional and allow finer control of the restore processs. +func Restore(ctx context.Context, nameOrId string, keep, tcpEstablished, ignoreRootFS, ignoreStaticIP, ignoreStaticMAC *bool, name, importArchive *string) (*entities.RestoreReport, error) { + var report entities.RestoreReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if keep != nil { + params.Set("keep", strconv.FormatBool(*keep)) + } + if tcpEstablished != nil { + params.Set("TCPestablished", strconv.FormatBool(*tcpEstablished)) + } + if ignoreRootFS != nil { + params.Set("ignoreRootFS", strconv.FormatBool(*ignoreRootFS)) + } + if ignoreStaticIP != nil { + params.Set("ignoreStaticIP", strconv.FormatBool(*ignoreStaticIP)) + } + if ignoreStaticMAC != nil { + params.Set("ignoreStaticMAC", strconv.FormatBool(*ignoreStaticMAC)) + } + if name != nil { + params.Set("name", *name) + } + if importArchive != nil { + params.Set("import", *importArchive) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restore", params, nameOrId) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index a5b74013b..78f592d32 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -1,6 +1,4 @@ -// +build !remoteclient - -package adapter +package checkpoint import ( "context" @@ -42,9 +40,9 @@ func crImportFromJSON(filePath string, v interface{}) error { return nil } -// crImportCheckpoint it the function which imports the information +// CRImportCheckpoint it the function which imports the information // from checkpoint tarball and re-creates the container from that information -func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { +func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { // First get the container definition from the // tarball to a temporary directory archiveFile, err := os.Open(input) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index d51124f55..45dea98bf 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -121,3 +121,35 @@ type CommitReport struct { type ContainerExportOptions struct { Output string } + +type CheckpointOptions struct { + All bool + Export string + IgnoreRootFS bool + Keep bool + Latest bool + LeaveRuninng bool + TCPEstablished bool +} + +type CheckpointReport struct { + Err error + Id string +} + +type RestoreOptions struct { + All bool + IgnoreRootFS bool + IgnoreStaticIP bool + IgnoreStaticMAC bool + Import string + Keep bool + Latest bool + Name string + TCPEstablished bool +} + +type RestoreReport struct { + Err error + Id string +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index a122857cd..3aaa7136f 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -8,6 +8,8 @@ import ( type ContainerEngine interface { ContainerCommit(ctx context.Context, nameOrId string, options CommitOptions) (*CommitReport, error) + ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) + ContainerRestore(ctx context.Context, namesOrIds []string, options RestoreOptions) ([]*RestoreReport, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]*ContainerInspectReport, error) ContainerExport(ctx context.Context, nameOrId string, options ContainerExportOptions) error diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d4c5ac311..3c38b2093 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -13,12 +13,42 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/checkpoint" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/signal" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) +// getContainersByContext gets pods whether all, latest, or a slice of names/ids +// is specified. +func getContainersByContext(all, latest bool, names []string, runtime *libpod.Runtime) (ctrs []*libpod.Container, err error) { + var ctr *libpod.Container + ctrs = []*libpod.Container{} + + switch { + case all: + ctrs, err = runtime.GetAllContainers() + case latest: + ctr, err = runtime.GetLatestContainer() + ctrs = append(ctrs, ctr) + default: + for _, n := range names { + ctr, e := runtime.LookupContainer(n) + if e != nil { + // Log all errors here, so callers don't need to. + logrus.Debugf("Error looking up container %q: %v", n, e) + if err == nil { + err = e + } + } else { + ctrs = append(ctrs, ctr) + } + } + } + return +} + // TODO: Should return *entities.ContainerExistsReport, error func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { _, err := ic.Libpod.LookupContainer(nameOrId) @@ -333,3 +363,82 @@ func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, } return ctr.Export(options.Output) } + +func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options entities.CheckpointOptions) ([]*entities.CheckpointReport, error) { + var ( + err error + cons []*libpod.Container + reports []*entities.CheckpointReport + ) + checkOpts := libpod.ContainerCheckpointOptions{ + Keep: options.Keep, + TCPEstablished: options.TCPEstablished, + TargetFile: options.Export, + IgnoreRootfs: options.IgnoreRootFS, + } + + if options.All { + running := func(c *libpod.Container) bool { + state, _ := c.State() + return state == define.ContainerStateRunning + } + cons, err = ic.Libpod.GetContainers(running) + } else { + cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, con := range cons { + err = con.Checkpoint(ctx, checkOpts) + reports = append(reports, &entities.CheckpointReport{ + Err: err, + Id: con.ID(), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) { + var ( + cons []*libpod.Container + err error + filterFuncs []libpod.ContainerFilter + reports []*entities.RestoreReport + ) + + restoreOptions := libpod.ContainerCheckpointOptions{ + Keep: options.Keep, + TCPEstablished: options.TCPEstablished, + TargetFile: options.Import, + Name: options.Name, + IgnoreRootfs: options.IgnoreRootFS, + IgnoreStaticIP: options.IgnoreStaticIP, + IgnoreStaticMAC: options.IgnoreStaticMAC, + } + + filterFuncs = append(filterFuncs, func(c *libpod.Container) bool { + state, _ := c.State() + return state == define.ContainerStateExited + }) + + switch { + case options.Import != "": + cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options.Import, options.Name) + case options.All: + cons, err = ic.Libpod.GetContainers(filterFuncs...) + default: + cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, con := range cons { + err := con.Restore(ctx, restoreOptions) + reports = append(reports, &entities.RestoreReport{ + Err: err, + Id: con.ID(), + }) + } + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 8885ae7c7..5832d41be 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -6,7 +6,8 @@ import ( "os" "github.com/containers/image/v5/docker/reference" - + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -226,3 +227,72 @@ func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, } return containers.Export(ic.ClientCxt, nameOrId, w) } + +func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options entities.CheckpointOptions) ([]*entities.CheckpointReport, error) { + var ( + reports []*entities.CheckpointReport + err error + ctrs []libpod.ListContainer + ) + + if options.All { + allCtrs, err := getContainersByContext(ic.ClientCxt, true, []string{}) + if err != nil { + return nil, err + } + // narrow the list to running only + for _, c := range allCtrs { + if c.State == define.ContainerStateRunning.String() { + ctrs = append(ctrs, c) + } + } + + } else { + ctrs, err = getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + } + for _, c := range ctrs { + report, err := containers.Checkpoint(ic.ClientCxt, c.ID, &options.Keep, &options.LeaveRuninng, &options.TCPEstablished, &options.IgnoreRootFS, &options.Export) + if err != nil { + reports = append(reports, &entities.CheckpointReport{Id: c.ID, Err: err}) + } + reports = append(reports, report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) { + var ( + reports []*entities.RestoreReport + err error + ctrs []libpod.ListContainer + ) + if options.All { + allCtrs, err := getContainersByContext(ic.ClientCxt, true, []string{}) + if err != nil { + return nil, err + } + // narrow the list to exited only + for _, c := range allCtrs { + if c.State == define.ContainerStateExited.String() { + ctrs = append(ctrs, c) + } + } + + } else { + ctrs, err = getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + } + for _, c := range ctrs { + report, err := containers.Restore(ic.ClientCxt, c.ID, &options.Keep, &options.TCPEstablished, &options.IgnoreRootFS, &options.IgnoreStaticIP, &options.IgnoreStaticMAC, &options.Name, &options.Import) + if err != nil { + reports = append(reports, &entities.RestoreReport{Id: c.ID, Err: err}) + } + reports = append(reports, report) + } + return reports, nil +} |