github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/checkpoint.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "net" 7 "os" 8 "path/filepath" 9 "strconv" 10 11 criu "github.com/checkpoint-restore/go-criu/v6/rpc" 12 "github.com/opencontainers/runc/libcontainer" 13 "github.com/opencontainers/runc/libcontainer/userns" 14 "github.com/opencontainers/runtime-spec/specs-go" 15 "github.com/sirupsen/logrus" 16 "github.com/urfave/cli" 17 "golang.org/x/sys/unix" 18 ) 19 20 var checkpointCommand = cli.Command{ 21 Name: "checkpoint", 22 Usage: "checkpoint a running container", 23 ArgsUsage: `<container-id> 24 25 Where "<container-id>" is the name for the instance of the container to be 26 checkpointed.`, 27 Description: `The checkpoint command saves the state of the container instance.`, 28 Flags: []cli.Flag{ 29 cli.StringFlag{Name: "image-path", Value: "", Usage: "path for saving criu image files"}, 30 cli.StringFlag{Name: "work-path", Value: "", Usage: "path for saving work files and logs"}, 31 cli.StringFlag{Name: "parent-path", Value: "", Usage: "path for previous criu image files in pre-dump"}, 32 cli.BoolFlag{Name: "leave-running", Usage: "leave the process running after checkpointing"}, 33 cli.BoolFlag{Name: "tcp-established", Usage: "allow open tcp connections"}, 34 cli.BoolFlag{Name: "ext-unix-sk", Usage: "allow external unix sockets"}, 35 cli.BoolFlag{Name: "shell-job", Usage: "allow shell jobs"}, 36 cli.BoolFlag{Name: "lazy-pages", Usage: "use userfaultfd to lazily restore memory pages"}, 37 cli.IntFlag{Name: "status-fd", Value: -1, Usage: "criu writes \\0 to this FD once lazy-pages is ready"}, 38 cli.StringFlag{Name: "page-server", Value: "", Usage: "ADDRESS:PORT of the page server"}, 39 cli.BoolFlag{Name: "file-locks", Usage: "handle file locks, for safety"}, 40 cli.BoolFlag{Name: "pre-dump", Usage: "dump container's memory information only, leave the container running after this"}, 41 cli.StringFlag{Name: "manage-cgroups-mode", Value: "", Usage: "cgroups mode: soft|full|strict|ignore (default: soft)"}, 42 cli.StringSliceFlag{Name: "empty-ns", Usage: "create a namespace, but don't restore its properties"}, 43 cli.BoolFlag{Name: "auto-dedup", Usage: "enable auto deduplication of memory images"}, 44 }, 45 Action: func(context *cli.Context) error { 46 if err := checkArgs(context, 1, exactArgs); err != nil { 47 return err 48 } 49 // XXX: Currently this is untested with rootless containers. 50 if os.Geteuid() != 0 || userns.RunningInUserNS() { 51 logrus.Warn("runc checkpoint is untested with rootless containers") 52 } 53 54 container, err := getContainer(context) 55 if err != nil { 56 return err 57 } 58 status, err := container.Status() 59 if err != nil { 60 return err 61 } 62 if status == libcontainer.Created || status == libcontainer.Stopped { 63 return fmt.Errorf("Container cannot be checkpointed in %s state", status.String()) 64 } 65 options, err := criuOptions(context) 66 if err != nil { 67 return err 68 } 69 70 err = container.Checkpoint(options) 71 if err == nil && !(options.LeaveRunning || options.PreDump) { 72 // Destroy the container unless we tell CRIU to keep it. 73 if err := container.Destroy(); err != nil { 74 logrus.Warn(err) 75 } 76 } 77 return err 78 }, 79 } 80 81 func prepareImagePaths(context *cli.Context) (string, string, error) { 82 imagePath := context.String("image-path") 83 if imagePath == "" { 84 imagePath = getDefaultImagePath() 85 } 86 87 if err := os.MkdirAll(imagePath, 0o600); err != nil { 88 return "", "", err 89 } 90 91 parentPath := context.String("parent-path") 92 if parentPath == "" { 93 return imagePath, parentPath, nil 94 } 95 96 if filepath.IsAbs(parentPath) { 97 return "", "", errors.New("--parent-path must be relative") 98 } 99 100 realParent := filepath.Join(imagePath, parentPath) 101 fi, err := os.Stat(realParent) 102 if err == nil && !fi.IsDir() { 103 err = &os.PathError{Path: realParent, Err: unix.ENOTDIR} 104 } 105 106 if err != nil { 107 return "", "", fmt.Errorf("invalid --parent-path: %w", err) 108 } 109 110 return imagePath, parentPath, nil 111 } 112 113 func criuOptions(context *cli.Context) (*libcontainer.CriuOpts, error) { 114 imagePath, parentPath, err := prepareImagePaths(context) 115 if err != nil { 116 return nil, err 117 } 118 119 opts := &libcontainer.CriuOpts{ 120 ImagesDirectory: imagePath, 121 WorkDirectory: context.String("work-path"), 122 ParentImage: parentPath, 123 LeaveRunning: context.Bool("leave-running"), 124 TcpEstablished: context.Bool("tcp-established"), 125 ExternalUnixConnections: context.Bool("ext-unix-sk"), 126 ShellJob: context.Bool("shell-job"), 127 FileLocks: context.Bool("file-locks"), 128 PreDump: context.Bool("pre-dump"), 129 AutoDedup: context.Bool("auto-dedup"), 130 LazyPages: context.Bool("lazy-pages"), 131 StatusFd: context.Int("status-fd"), 132 LsmProfile: context.String("lsm-profile"), 133 LsmMountContext: context.String("lsm-mount-context"), 134 } 135 136 // CRIU options below may or may not be set. 137 138 if psOpt := context.String("page-server"); psOpt != "" { 139 address, port, err := net.SplitHostPort(psOpt) 140 141 if err != nil || address == "" || port == "" { 142 return nil, errors.New("Use --page-server ADDRESS:PORT to specify page server") 143 } 144 portInt, err := strconv.Atoi(port) 145 if err != nil { 146 return nil, errors.New("Invalid port number") 147 } 148 opts.PageServer = libcontainer.CriuPageServerInfo{ 149 Address: address, 150 Port: int32(portInt), 151 } 152 } 153 154 switch context.String("manage-cgroups-mode") { 155 case "": 156 // do nothing 157 case "soft": 158 opts.ManageCgroupsMode = criu.CriuCgMode_SOFT 159 case "full": 160 opts.ManageCgroupsMode = criu.CriuCgMode_FULL 161 case "strict": 162 opts.ManageCgroupsMode = criu.CriuCgMode_STRICT 163 case "ignore": 164 opts.ManageCgroupsMode = criu.CriuCgMode_IGNORE 165 default: 166 return nil, errors.New("Invalid manage-cgroups-mode value") 167 } 168 169 // runc doesn't manage network devices and their configuration. 170 nsmask := unix.CLONE_NEWNET 171 172 if context.IsSet("empty-ns") { 173 namespaceMapping := map[specs.LinuxNamespaceType]int{ 174 specs.NetworkNamespace: unix.CLONE_NEWNET, 175 } 176 177 for _, ns := range context.StringSlice("empty-ns") { 178 f, exists := namespaceMapping[specs.LinuxNamespaceType(ns)] 179 if !exists { 180 return nil, fmt.Errorf("namespace %q is not supported", ns) 181 } 182 nsmask |= f 183 } 184 } 185 186 opts.EmptyNs = uint32(nsmask) 187 188 return opts, nil 189 }