| package main |
| |
| import ( |
| "errors" |
| "fmt" |
| "net" |
| "os" |
| "path/filepath" |
| "strconv" |
| |
| criu "github.com/checkpoint-restore/go-criu/v6/rpc" |
| "github.com/moby/sys/userns" |
| "github.com/opencontainers/runtime-spec/specs-go" |
| "github.com/sirupsen/logrus" |
| "github.com/urfave/cli" |
| "golang.org/x/sys/unix" |
| |
| "github.com/opencontainers/runc/libcontainer" |
| ) |
| |
| var checkpointCommand = cli.Command{ |
| Name: "checkpoint", |
| Usage: "checkpoint a running container", |
| ArgsUsage: `<container-id> |
| |
| Where "<container-id>" is the name for the instance of the container to be |
| checkpointed.`, |
| Description: `The checkpoint command saves the state of the container instance.`, |
| Flags: []cli.Flag{ |
| cli.StringFlag{Name: "image-path", Value: "", Usage: "path for saving criu image files"}, |
| cli.StringFlag{Name: "work-path", Value: "", Usage: "path for saving work files and logs"}, |
| cli.StringFlag{Name: "parent-path", Value: "", Usage: "path for previous criu image files in pre-dump"}, |
| cli.BoolFlag{Name: "leave-running", Usage: "leave the process running after checkpointing"}, |
| cli.BoolFlag{Name: "tcp-established", Usage: "allow open tcp connections"}, |
| cli.BoolFlag{Name: "ext-unix-sk", Usage: "allow external unix sockets"}, |
| cli.BoolFlag{Name: "shell-job", Usage: "allow shell jobs"}, |
| cli.BoolFlag{Name: "lazy-pages", Usage: "use userfaultfd to lazily restore memory pages"}, |
| cli.IntFlag{Name: "status-fd", Value: -1, Usage: "criu writes \\0 to this FD once lazy-pages is ready"}, |
| cli.StringFlag{Name: "page-server", Value: "", Usage: "ADDRESS:PORT of the page server"}, |
| cli.BoolFlag{Name: "file-locks", Usage: "handle file locks, for safety"}, |
| cli.BoolFlag{Name: "pre-dump", Usage: "dump container's memory information only, leave the container running after this"}, |
| cli.StringFlag{Name: "manage-cgroups-mode", Value: "", Usage: "cgroups mode: soft|full|strict|ignore (default: soft)"}, |
| cli.StringSliceFlag{Name: "empty-ns", Usage: "create a namespace, but don't restore its properties"}, |
| cli.BoolFlag{Name: "auto-dedup", Usage: "enable auto deduplication of memory images"}, |
| }, |
| Action: func(context *cli.Context) error { |
| if err := checkArgs(context, 1, exactArgs); err != nil { |
| return err |
| } |
| // XXX: Currently this is untested with rootless containers. |
| if os.Geteuid() != 0 || userns.RunningInUserNS() { |
| logrus.Warn("runc checkpoint is untested with rootless containers") |
| } |
| |
| container, err := getContainer(context) |
| if err != nil { |
| return err |
| } |
| status, err := container.Status() |
| if err != nil { |
| return err |
| } |
| if status == libcontainer.Created || status == libcontainer.Stopped { |
| return fmt.Errorf("Container cannot be checkpointed in %s state", status.String()) |
| } |
| options, err := criuOptions(context) |
| if err != nil { |
| return err |
| } |
| |
| err = container.Checkpoint(options) |
| if err == nil && !(options.LeaveRunning || options.PreDump) { |
| // Destroy the container unless we tell CRIU to keep it. |
| if err := container.Destroy(); err != nil { |
| logrus.Warn(err) |
| } |
| } |
| return err |
| }, |
| } |
| |
| func prepareImagePaths(context *cli.Context) (string, string, error) { |
| imagePath := context.String("image-path") |
| if imagePath == "" { |
| imagePath = getDefaultImagePath() |
| } |
| |
| if err := os.MkdirAll(imagePath, 0o600); err != nil { |
| return "", "", err |
| } |
| |
| parentPath := context.String("parent-path") |
| if parentPath == "" { |
| return imagePath, parentPath, nil |
| } |
| |
| if filepath.IsAbs(parentPath) { |
| return "", "", errors.New("--parent-path must be relative") |
| } |
| |
| realParent := filepath.Join(imagePath, parentPath) |
| fi, err := os.Stat(realParent) |
| if err == nil && !fi.IsDir() { |
| err = &os.PathError{Path: realParent, Err: unix.ENOTDIR} |
| } |
| |
| if err != nil { |
| return "", "", fmt.Errorf("invalid --parent-path: %w", err) |
| } |
| |
| return imagePath, parentPath, nil |
| } |
| |
| func criuOptions(context *cli.Context) (*libcontainer.CriuOpts, error) { |
| imagePath, parentPath, err := prepareImagePaths(context) |
| if err != nil { |
| return nil, err |
| } |
| |
| opts := &libcontainer.CriuOpts{ |
| ImagesDirectory: imagePath, |
| WorkDirectory: context.String("work-path"), |
| ParentImage: parentPath, |
| LeaveRunning: context.Bool("leave-running"), |
| TcpEstablished: context.Bool("tcp-established"), |
| ExternalUnixConnections: context.Bool("ext-unix-sk"), |
| ShellJob: context.Bool("shell-job"), |
| FileLocks: context.Bool("file-locks"), |
| PreDump: context.Bool("pre-dump"), |
| AutoDedup: context.Bool("auto-dedup"), |
| LazyPages: context.Bool("lazy-pages"), |
| StatusFd: context.Int("status-fd"), |
| LsmProfile: context.String("lsm-profile"), |
| LsmMountContext: context.String("lsm-mount-context"), |
| } |
| |
| // CRIU options below may or may not be set. |
| |
| if psOpt := context.String("page-server"); psOpt != "" { |
| address, port, err := net.SplitHostPort(psOpt) |
| |
| if err != nil || address == "" || port == "" { |
| return nil, errors.New("Use --page-server ADDRESS:PORT to specify page server") |
| } |
| portInt, err := strconv.Atoi(port) |
| if err != nil { |
| return nil, errors.New("Invalid port number") |
| } |
| opts.PageServer = libcontainer.CriuPageServerInfo{ |
| Address: address, |
| Port: int32(portInt), |
| } |
| } |
| |
| switch context.String("manage-cgroups-mode") { |
| case "": |
| // do nothing |
| case "soft": |
| opts.ManageCgroupsMode = criu.CriuCgMode_SOFT |
| case "full": |
| opts.ManageCgroupsMode = criu.CriuCgMode_FULL |
| case "strict": |
| opts.ManageCgroupsMode = criu.CriuCgMode_STRICT |
| case "ignore": |
| opts.ManageCgroupsMode = criu.CriuCgMode_IGNORE |
| default: |
| return nil, errors.New("Invalid manage-cgroups-mode value") |
| } |
| |
| // runc doesn't manage network devices and their configuration. |
| nsmask := unix.CLONE_NEWNET |
| |
| if context.IsSet("empty-ns") { |
| namespaceMapping := map[specs.LinuxNamespaceType]int{ |
| specs.NetworkNamespace: unix.CLONE_NEWNET, |
| } |
| |
| for _, ns := range context.StringSlice("empty-ns") { |
| f, exists := namespaceMapping[specs.LinuxNamespaceType(ns)] |
| if !exists { |
| return nil, fmt.Errorf("namespace %q is not supported", ns) |
| } |
| nsmask |= f |
| } |
| } |
| |
| opts.EmptyNs = uint32(nsmask) |
| |
| return opts, nil |
| } |