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  }