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/v5/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' (default), 'full' and 'strict'"},
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
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 fatal(fmt.Errorf("Container cannot be checkpointed in %s state", status.String()))
64 }
65 options := criuOptions(context)
66 if !(options.LeaveRunning || options.PreDump) {
67
68 defer destroy(container)
69 }
70
71 setPageServer(context, options)
72 setManageCgroupsMode(context, options)
73 if err := setEmptyNsMask(context, options); err != nil {
74 return err
75 }
76 return container.Checkpoint(options)
77 },
78 }
79
80 func prepareImagePaths(context *cli.Context) (string, string, error) {
81 imagePath := context.String("image-path")
82 if imagePath == "" {
83 imagePath = getDefaultImagePath()
84 }
85
86 if err := os.MkdirAll(imagePath, 0o600); err != nil {
87 return "", "", err
88 }
89
90 parentPath := context.String("parent-path")
91 if parentPath == "" {
92 return imagePath, parentPath, nil
93 }
94
95 if filepath.IsAbs(parentPath) {
96 return "", "", errors.New("--parent-path must be relative")
97 }
98
99 realParent := filepath.Join(imagePath, parentPath)
100 fi, err := os.Stat(realParent)
101 if err == nil && !fi.IsDir() {
102 err = &os.PathError{Path: realParent, Err: unix.ENOTDIR}
103 }
104
105 if err != nil {
106 return "", "", fmt.Errorf("invalid --parent-path: %w", err)
107 }
108
109 return imagePath, parentPath, nil
110 }
111
112 func setPageServer(context *cli.Context, options *libcontainer.CriuOpts) {
113
114
115 if psOpt := context.String("page-server"); psOpt != "" {
116 address, port, err := net.SplitHostPort(psOpt)
117
118 if err != nil || address == "" || port == "" {
119 fatal(errors.New("Use --page-server ADDRESS:PORT to specify page server"))
120 }
121 portInt, err := strconv.Atoi(port)
122 if err != nil {
123 fatal(errors.New("Invalid port number"))
124 }
125 options.PageServer = libcontainer.CriuPageServerInfo{
126 Address: address,
127 Port: int32(portInt),
128 }
129 }
130 }
131
132 func setManageCgroupsMode(context *cli.Context, options *libcontainer.CriuOpts) {
133 if cgOpt := context.String("manage-cgroups-mode"); cgOpt != "" {
134 switch cgOpt {
135 case "soft":
136 options.ManageCgroupsMode = criu.CriuCgMode_SOFT
137 case "full":
138 options.ManageCgroupsMode = criu.CriuCgMode_FULL
139 case "strict":
140 options.ManageCgroupsMode = criu.CriuCgMode_STRICT
141 default:
142 fatal(errors.New("Invalid manage cgroups mode"))
143 }
144 }
145 }
146
147 var namespaceMapping = map[specs.LinuxNamespaceType]int{
148 specs.NetworkNamespace: unix.CLONE_NEWNET,
149 }
150
151 func setEmptyNsMask(context *cli.Context, options *libcontainer.CriuOpts) error {
152
153 nsmask := unix.CLONE_NEWNET
154
155 for _, ns := range context.StringSlice("empty-ns") {
156 f, exists := namespaceMapping[specs.LinuxNamespaceType(ns)]
157 if !exists {
158 return fmt.Errorf("namespace %q is not supported", ns)
159 }
160 nsmask |= f
161 }
162
163 options.EmptyNs = uint32(nsmask)
164 return nil
165 }
166
View as plain text