The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/4729
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) ===
From cb3f1097cdefda8842d734d4efd4e6042393fb4f Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Wed, 27 Jun 2018 20:52:57 +0200 Subject: [PATCH 1/3] lxc-to-lxd: Rewrite in Go Signed-off-by: Thomas Hipp <[email protected]> --- lxc-to-lxd/config.go | 214 ++++++++++++++++ lxc-to-lxd/main.go | 44 ++++ lxc-to-lxd/main_migrate.go | 602 +++++++++++++++++++++++++++++++++++++++++++++ lxc-to-lxd/main_netcat.go | 72 ++++++ lxc-to-lxd/network.go | 31 +++ lxc-to-lxd/transfer.go | 141 +++++++++++ lxc-to-lxd/utils.go | 182 ++++++++++++++ 7 files changed, 1286 insertions(+) create mode 100644 lxc-to-lxd/config.go create mode 100644 lxc-to-lxd/main.go create mode 100644 lxc-to-lxd/main_migrate.go create mode 100644 lxc-to-lxd/main_netcat.go create mode 100644 lxc-to-lxd/network.go create mode 100644 lxc-to-lxd/transfer.go create mode 100644 lxc-to-lxd/utils.go diff --git a/lxc-to-lxd/config.go b/lxc-to-lxd/config.go new file mode 100644 index 000000000..615e7eb11 --- /dev/null +++ b/lxc-to-lxd/config.go @@ -0,0 +1,214 @@ +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/lxc/lxd/shared" +) + +var checkedKeys = []string{ + "lxc.aa_allow_incomplete", + "lxc.aa_profile", + "lxc.apparmor.allow_incomplete", + "lxc.apparmor.profile", + "lxc.arch", + "lxc.autodev", + "lxc.cap.drop", + "lxc.environment", + "lxc.haltsignal", + "lxc.id_map", + "lxc.idmap", + "lxc.include", + "lxc.loglevel", + "lxc.mount", + "lxc.mount.auto", + "lxc.mount.entry", + "lxc.pts", + "lxc.pty.max", + "lxc.rebootsignal", + "lxc.rootfs", + "lxc.rootfs.backend", + "lxc.rootfs.mount", + "lxc.rootfs.path", + "lxc.seccomp", + "lxc.signal.halt", + "lxc.signal.reboot", + "lxc.signal.stop", + "lxc.start.auto", + "lxc.start.delay", + "lxc.start.order", + "lxc.stopsignal", + "lxc.tty", + "lxc.tty.max", + "lxc.uts.name", + "lxc.utsname", + "lxd.migrated", +} + +func getUnsupportedKeys(config []string) []string { + var out []string + + for _, a := range config { + supported := false + + for _, b := range checkedKeys { + if a == b { + supported = true + break + } + } + + if !supported { + out = append(out, a) + } + } + + return out +} + +func getConfig(config []string, key string) []string { + // Return an array since keys can be specified more than once + var out []string + + for _, c := range config { + text := strings.TrimSpace(c) + + // Ignore empty lines and comments + if len(text) == 0 || strings.HasPrefix(text, "#") { + continue + } + + line := strings.Split(text, "=") + k := strings.TrimSpace(line[0]) + v := strings.Trim(strings.TrimSpace(line[1]), "\"") + + if k == key && len(v) > 0 { + out = append(out, v) + } + } + + if len(out) == 0 { + return nil + } + + return out +} + +func getConfigKeys(config []string) []string { + // Make sure we don't have duplicate keys + m := make(map[string]bool, 0) + for _, c := range config { + text := strings.TrimSpace(c) + + // Ignore empty lines and comments + if len(text) == 0 || strings.HasPrefix(text, "#") { + continue + } + + line := strings.Split(text, "=") + key := strings.TrimSpace(line[0]) + if strings.HasPrefix(key, "lxc.") { + m[key] = true + } + } + + var out []string + for k := range m { + out = append(out, k) + } + + return out +} + +func parseConfig(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var config []string + + // Parse config + sc := bufio.NewScanner(file) + for sc.Scan() { + text := strings.TrimSpace(sc.Text()) + + // Ignore empty lines and comments + if len(text) == 0 || strings.HasPrefix(text, "#") { + continue + } + + line := strings.Split(text, "=") + key := strings.TrimSpace(line[0]) + value := strings.TrimSpace(line[1]) + + switch key { + // Parse user-added includes + case "lxc.include": + // Ignore our own default configs + if strings.HasPrefix(value, "/usr/share/lxc/config/") { + continue + } + + if shared.PathExists(value) { + if shared.IsDir(value) { + files, err := ioutil.ReadDir(value) + if err != nil { + return nil, err + } + + for _, file := range files { + path := filepath.Join(value, file.Name()) + if !strings.HasSuffix(path, ".conf") { + continue + } + + config = append(config, path) + } + } else { + c, err := parseConfig(value) + if err != nil { + return nil, err + } + + config = append(config, c...) + } + continue + } + // Expand any fstab + case "lxc.mount": + if !shared.PathExists(value) { + fmt.Println("Container fstab file doesn't exist, skipping...") + continue + } + + file, err := os.Open(value) + if err != nil { + return nil, err + } + defer file.Close() + + sc := bufio.NewScanner(file) + for sc.Scan() { + text := strings.TrimSpace(sc.Text()) + + if len(text) > 0 && !strings.HasPrefix(text, "#") { + config = append(config, fmt.Sprintf("lxc.mount.entry = %s", text)) + } + } + + continue + + default: + config = append(config, text) + } + } + + return config, nil +} diff --git a/lxc-to-lxd/main.go b/lxc-to-lxd/main.go new file mode 100644 index 000000000..a350214ed --- /dev/null +++ b/lxc-to-lxd/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/lxc/lxd/shared/version" +) + +type cmdGlobal struct { + flagVersion bool + flagHelp bool +} + +func main() { + // migrate command (main) + migrateCmd := cmdMigrate{} + app := migrateCmd.Command() + app.SilenceUsage = true + + // Workaround for main command + app.Args = cobra.ArbitraryArgs + + // Global flags + globalCmd := cmdGlobal{} + migrateCmd.global = &globalCmd + app.PersistentFlags().BoolVar(&globalCmd.flagVersion, "version", false, "Print version number") + app.PersistentFlags().BoolVarP(&globalCmd.flagHelp, "help", "h", false, "Print help") + + // Version handling + app.SetVersionTemplate("{{.Version}}\n") + app.Version = version.Version + + // netcat sub-command + netcatCmd := cmdNetcat{global: &globalCmd} + app.AddCommand(netcatCmd.Command()) + + // Run the main command and handle errors + err := app.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/lxc-to-lxd/main_migrate.go b/lxc-to-lxd/main_migrate.go new file mode 100644 index 000000000..f973e3b21 --- /dev/null +++ b/lxc-to-lxd/main_migrate.go @@ -0,0 +1,602 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "strconv" + "strings" + + "github.com/spf13/cobra" + lxc "gopkg.in/lxc/go-lxc.v2" + + lxd "github.com/lxc/lxd/client" + "github.com/lxc/lxd/lxc/config" + "github.com/lxc/lxd/lxc/utils" + "github.com/lxc/lxd/lxd/types" + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/api" + "github.com/lxc/lxd/shared/i18n" + "github.com/lxc/lxd/shared/osarch" +) + +type cmdMigrate struct { + global *cmdGlobal + + conf *config.Config + confPath string + cmd *cobra.Command + + // Flags + flagDryRun bool + flagDebug bool + flagAll bool + flagDelete bool + flagStorage string + flagLXCPath string + flagRsyncArgs string + flagContainers []string +} + +func (c *cmdMigrate) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "lxc-to-lxd", + Short: i18n.G("Command line client for container migration"), + } + + // Wrappers + cmd.RunE = c.RunE + + // Flags + cmd.Flags().BoolVar(&c.flagDryRun, "dry-run", false, i18n.G("Dry run mode")) + cmd.Flags().BoolVar(&c.flagDebug, "debug", false, i18n.G("Print debugging output")) + cmd.Flags().BoolVar(&c.flagAll, "all", false, i18n.G("Import all containers")) + cmd.Flags().BoolVar(&c.flagDelete, "delete", false, i18n.G("Delete the source container")) + cmd.Flags().StringVar(&c.flagStorage, "storage", "", + i18n.G("Storage pool to use for the container")+"``") + cmd.Flags().StringVar(&c.flagLXCPath, "lxcpath", lxc.DefaultConfigPath(), + i18n.G("Alternate LXC path")+"``") + cmd.Flags().StringVar(&c.flagRsyncArgs, "rsync-args", "", + "Extra arguments to pass to rsync"+"``") + cmd.Flags().StringSliceVar(&c.flagContainers, "containers", nil, + i18n.G("Container(s) to import")+"``") + + return cmd +} + +func (c *cmdMigrate) RunE(cmd *cobra.Command, args []string) error { + if (len(c.flagContainers) == 0 && !c.flagAll) || (len(c.flagContainers) > 0 && c.flagAll) { + fmt.Fprintln(os.Stderr, "You must either pass container names or --all") + os.Exit(1) + } + // Connect to LXD + d, err := lxd.ConnectLXDUnix("", nil) + if err != nil { + return err + } + + // Retrieve LXC containers + for _, container := range lxc.Containers(c.flagLXCPath) { + if !c.flagAll && !shared.StringInSlice(container.Name(), c.flagContainers) { + continue + } + + err := convertContainer(d, container, c.flagStorage, + c.flagDryRun, c.flagRsyncArgs, c.flagDebug) + if err != nil { + fmt.Printf("Skipping container '%s': %v\n", container.Name(), err) + continue + } + + // Delete container + if c.flagDelete { + if c.flagDryRun { + fmt.Println("Would destroy container now") + } else { + err := container.Destroy() + if err != nil { + fmt.Printf("Failed to destroy container '%s': %v\n", container.Name(), err) + } + } + } + } + + return nil +} + +func validateConfig(conf []string, container *lxc.Container) error { + // Checking whether container has already been migrated + fmt.Println("Checking whether container has already been migrated") + if len(getConfig(conf, "lxd.migrated")) > 0 { + return fmt.Errorf("Container has already been migrated") + } + + // Validate lxc.utsname / lxc.uts.name + value := getConfig(conf, "lxc.uts.name") + if value == nil { + value = getConfig(conf, "lxc.utsname") + } + if value == nil || value[0] != container.Name() { + return fmt.Errorf("Container name doesn't match lxc.uts.name / lxc.utsname") + } + + // Validate lxc.aa_allow_incomplete: must be set to 0 or unset. + fmt.Println("Validating whether incomplete AppArmor support is enabled") + value = getConfig(conf, "lxc.apparmor.allow_incomplete") + if value == nil { + value = getConfig(conf, "lxc.aa_allow_incomplete") + } + if value != nil { + v, err := strconv.Atoi(value[0]) + if err != nil { + return err + } + + if v != 0 { + return fmt.Errorf("Container allows incomplete AppArmor support") + } + } + + // Validate lxc.autodev: must be set to 1 or unset. + fmt.Println("Validating whether mounting a minimal /dev is enabled") + value = getConfig(conf, "lxc.autodev") + if value != nil { + v, err := strconv.Atoi(value[0]) + if err != nil { + return err + } + + if v != 1 { + return fmt.Errorf("Container doesn't mount a minimal /dev filesystem") + } + } + + // Extract and valid rootfs key + fmt.Println("Validating container rootfs") + rootfs, err := getRootfs(conf) + if err != nil { + return err + } + + if !shared.PathExists(rootfs) { + return fmt.Errorf("Couldn't find the container rootfs '%s'", rootfs) + } + + return nil +} + +func convertContainer(d lxd.ContainerServer, container *lxc.Container, storage string, + dryRun bool, rsyncArgs string, debug bool) error { + // Don't migrate running containers + if container.Running() { + return fmt.Errorf("Only stopped containers can be migrated") + } + + fmt.Println("Parsing LXC configuration") + conf, err := parseConfig(container.ConfigFileName()) + if err != nil { + return err + } + + if debug { + fmt.Printf("Container configuration:\n %v\n", strings.Join(conf, "\n ")) + } + + // Check whether there are unsupported keys in the config + fmt.Println("Checking for unsupported LXC configuration keys") + keys := getUnsupportedKeys(getConfigKeys(conf)) + for _, k := range keys { + if !strings.HasPrefix(k, "lxc.net.") && + !strings.HasPrefix(k, "lxc.network.") && + !strings.HasPrefix(k, "lxc.cgroup.") && + !strings.HasPrefix(k, "lxc.cgroup2.") { + return fmt.Errorf("Found unsupported config key '%s'", k) + } + } + + // Make sure we don't have a conflict + fmt.Println("Checking for existing containers") + containers, err := d.GetContainerNames() + if err != nil { + return err + } + + found := false + for _, name := range containers { + if name == container.Name() { + found = true + } + } + if found { + return fmt.Errorf("Container already exists") + } + + // Validate config + err = validateConfig(conf, container) + if err != nil { + return err + } + + newConfig := make(map[string]string, 0) + + value := getConfig(conf, "lxd.idmap") + if value == nil { + value = getConfig(conf, "lxd.id_map") + } + if value == nil { + // Privileged container + newConfig["security.privileged"] = "true" + } else { + // Unprivileged container + newConfig["security.privileged"] = "false" + } + + newDevices := make(types.Devices, 0) + + // Convert network configuration + err = convertNetworkConfig(container, newDevices) + if err != nil { + return err + } + + // Convert storage configuration + err = convertStorageConfig(conf, newDevices) + if err != nil { + return err + } + + // Convert environment + fmt.Println("Processing environment configuration") + value = getConfig(conf, "lxc.environment") + for _, env := range value { + entry := strings.Split(env, "=") + key := strings.TrimSpace(entry[0]) + val := strings.TrimSpace(entry[len(entry)-1]) + newConfig[fmt.Sprintf("environment.%s", key)] = val + } + + // Convert auto-start + fmt.Println("Processing container boot configuration") + value = getConfig(conf, "lxc.start.auto") + if value != nil { + val, err := strconv.Atoi(value[0]) + if err != nil { + return err + } + + if val > 0 { + newConfig["boot.autostart"] = "true" + } + } + + value = getConfig(conf, "lxc.start.delay") + if value != nil { + val, err := strconv.Atoi(value[0]) + if err != nil { + return err + } + + if val > 0 { + newConfig["boot.autostart.delay"] = value[0] + } + } + + value = getConfig(conf, "lxc.start.order") + if value != nil { + val, err := strconv.Atoi(value[0]) + if err != nil { + return err + } + + if val > 0 { + newConfig["boot.autostart.priority"] = value[0] + } + } + + // Convert apparmor + fmt.Println("Processing container apparmor configuration") + value = getConfig(conf, "lxc.apparmor.profile") + if value == nil { + value = getConfig(conf, "lxc.aa_profile") + } + if value != nil { + if value[0] == "lxc-container-default-with-nesting" { + newConfig["security.nesting"] = "true" + } else if value[0] != "lxc-container-default" { + newConfig["raw.lxc"] = fmt.Sprintf("lxc.aa_profile=%s\n", value[0]) + } + } + + // Convert seccomp + fmt.Println("Processing container seccomp configuration") + value = getConfig(conf, "lxc.seccomp.profile") + if value == nil { + value = getConfig(conf, "lxc.seccomp") + } + if value != nil && value[0] != "/usr/share/lxc/config/common.seccomp" { + return fmt.Errorf("Custom seccomp profiles aren't supported") + } + + // Convert SELinux + fmt.Println("Processing container SELinux configuration") + value = getConfig(conf, "lxc.selinux.context") + if value == nil { + value = getConfig(conf, "lxc.se_context") + } + if value != nil { + return fmt.Errorf("Custom SELinux policies aren't supported") + } + + // Convert capabilities + fmt.Println("Processing container capabilities configuration") + value = getConfig(conf, "lxc.cap.drop") + if value != nil { + for _, cap := range strings.Split(value[0], " ") { + // Ignore capabilities that are dropped in LXD containers by default. + if shared.StringInSlice(cap, []string{"mac_admin", "mac_override", "sys_module", + "sys_time"}) { + continue + } + return fmt.Errorf("Custom capabilities aren't supported") + } + } + + value = getConfig(conf, "lxc.cap.keep") + if value != nil { + return fmt.Errorf("Custom capabilities aren't supported") + } + + // Add rest of the keys to lxc.raw + for _, c := range conf { + parts := strings.SplitN(c, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + + switch key { + case "lxc.signal.halt", "lxc.haltsignal": + newConfig["raw.lxc"] += fmt.Sprintf("lxc.signal.halt=%s\n", val) + case "lxc.signal.reboot", "lxc.rebootsignal": + newConfig["raw.lxc"] += fmt.Sprintf("lxc.signal.reboot=%s\n", val) + case "lxc.signal.stop", "lxc.stopsignal": + newConfig["raw.lxc"] += fmt.Sprintf("lxc.signal.stop=%s\n", val) + case "lxc.apparmor.allow_incomplete", "lxc.aa_allow_incomplete": + newConfig["raw.lxc"] += fmt.Sprintf("lxc.apparmor.allow_incomplete=%s\n", val) + case "lxc.pty.max", "lxc.pts": + newConfig["raw.lxc"] += fmt.Sprintf("lxc.pty.max=%s\n", val) + case "lxc.tty.max", "lxc.tty": + newConfig["raw.lxc"] += fmt.Sprintf("lxc.tty.max=%s\n", val) + } + } + + // Setup the container creation request + req := api.ContainersPost{ + Name: container.Name(), + Source: api.ContainerSource{ + Type: "migration", + Mode: "push", + }, + } + req.Config = newConfig + req.Devices = newDevices + req.Profiles = []string{"default"} + + // Set the container architecture if set in LXC + fmt.Println("Processing container architecture configuration") + var arch string + value = getConfig(conf, "lxc.arch") + if value == nil { + fmt.Println("Couldn't find container architecture, assuming native") + arch = runtime.GOARCH + } else { + arch = value[0] + } + + archId, err := osarch.ArchitectureId(arch) + if err != nil { + return err + } + + req.Architecture, err = osarch.ArchitectureName(archId) + if err != nil { + return err + } + + if storage != "" { + req.Devices["root"] = map[string]string{ + "type": "disk", + "pool": storage, + "path": "/", + } + } + + if debug { + out, _ := json.MarshalIndent(req, "", " ") + fmt.Printf("LXD container config:\n%v\n", string(out)) + } + + // Create container + fmt.Println("Creating container") + if dryRun { + fmt.Println("Would create container now") + } else { + op, err := d.CreateContainer(req) + if err != nil { + return err + } + + progress := utils.ProgressRenderer{Format: "Transferring container: %s"} + _, err = op.AddHandler(progress.UpdateOp) + if err != nil { + progress.Done("") + return err + } + + rootfs, _ := getRootfs(conf) + + err = transferRootfs(d, op, rootfs, rsyncArgs) + if err != nil { + return err + } + + progress.Done(fmt.Sprintf("Container '%s' successfully created", container.Name())) + } + + return nil +} + +func convertNetworkConfig(container *lxc.Container, devices types.Devices) error { + networkDevice := func(network map[string]string) map[string]string { + if network == nil { + return nil + } + + device := make(map[string]string, 0) + device["type"] = "nic" + + // Get the device type + device["nictype"] = network["type"] + + // Convert the configuration + for k, v := range network { + switch k { + case "hwaddr", "mtu", "name": + device[k] = v + case "link": + device["parent"] = v + case "veth_pair": + device["host_name"] = v + case "": + // empty key + return nil + } + } + + switch device["nictype"] { + case "veth": + _, ok := device["parent"] + if ok { + device["nictype"] = "bridged" + } else { + device["nictype"] = "p2p" + } + case "phys": + device["nictype"] = "physical" + case "empty": + return nil + } + + return device + } + + fmt.Println("Processing network configuration") + + devices["eth0"] = make(map[string]string, 0) + devices["eth0"]["type"] = "none" + + // New config key + for i, _ := range container.ConfigItem("lxc.net") { + network := networkGet(container, i, "lxc.net") + + dev := networkDevice(network) + if dev == nil { + continue + } + + devices[fmt.Sprintf("convert_net%d", i)] = dev + } + + // Old config key + for i, _ := range container.ConfigItem("lxc.network") { + network := networkGet(container, i, "lxc.network") + + dev := networkDevice(network) + if dev == nil { + continue + } + + devices[fmt.Sprintf("convert_net%d", len(devices))] = dev + } + + return nil +} + +func convertStorageConfig(conf []string, devices types.Devices) error { + fmt.Println("Processing storage configuration") + + i := 0 + for _, mount := range getConfig(conf, "lxc.mount.entry") { + parts := strings.Split(mount, " ") + if len(parts) < 4 { + return fmt.Errorf("Invalid mount configuration: %s", mount) + } + + // Ignore mounts that are present in LXD containers by default. + if shared.StringInSlice(parts[0], []string{"proc", "sysfs"}) { + continue + } + + device := make(map[string]string, 0) + device["type"] = "disk" + + // Deal with read-only mounts + if shared.StringInSlice("ro", strings.Split(parts[3], ",")) { + device["readonly"] = "true" + } + + // Deal with optional mounts + if shared.StringInSlice("optional", strings.Split(parts[3], ",")) { + device["optional"] = "true" + } else { + if strings.HasPrefix(parts[0], "/") { + if !shared.PathExists(parts[0]) { + return fmt.Errorf("Invalid path: %s", parts[0]) + } + } else { + continue + } + } + + // Set the source + device["source"] = parts[0] + + // Figure out the target + if !strings.HasPrefix(parts[1], "/") { + device["path"] = fmt.Sprintf("/%s", parts[1]) + } else { + rootfs, err := getRootfs(conf) + if err != nil { + return err + } + device["path"] = strings.TrimPrefix(parts[1], rootfs) + } + + devices[fmt.Sprintf("convert_mount%d", i)] = device + i++ + } + + return nil +} + +func getRootfs(conf []string) (string, error) { + value := getConfig(conf, "lxc.rootfs.path") + if value == nil { + value = getConfig(conf, "lxc.rootfs") + if value == nil { + return "", fmt.Errorf("Invalid container, missing lxc.rootfs key") + } + } + + // XXX: ignore the first part (storage) for now + parts := strings.Split(value[0], ":") + + if len(parts) != 2 { + return "", fmt.Errorf("Invalid container, invalid lxc.rootfs key") + } + + return parts[1], nil +} diff --git a/lxc-to-lxd/main_netcat.go b/lxc-to-lxd/main_netcat.go new file mode 100644 index 000000000..db21bed71 --- /dev/null +++ b/lxc-to-lxd/main_netcat.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "io" + "net" + "os" + "sync" + + "github.com/spf13/cobra" + + "github.com/lxc/lxd/shared/eagain" +) + +type cmdNetcat struct { + global *cmdGlobal +} + +func (c *cmdNetcat) Command() *cobra.Command { + cmd := &cobra.Command{} + + cmd.Use = "netcat <address>" + cmd.Short = "Sends stdin data to a unix socket" + cmd.RunE = c.Run + cmd.Hidden = true + + return cmd +} + +func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error { + // Help and usage + if len(args) == 0 { + cmd.Help() + return nil + } + + // Handle mandatory arguments + if len(args) != 1 { + cmd.Help() + return fmt.Errorf("Missing required argument") + } + + // Connect to the provided address + uAddr, err := net.ResolveUnixAddr("unix", args[0]) + if err != nil { + return err + } + + conn, err := net.DialUnix("unix", nil, uAddr) + if err != nil { + return err + } + + // We'll wait until we're done reading from the socket + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + io.Copy(eagain.Writer{Writer: os.Stdout}, eagain.Reader{Reader: conn}) + conn.Close() + wg.Done() + }() + + go func() { + io.Copy(eagain.Writer{Writer: conn}, eagain.Reader{Reader: os.Stdin}) + }() + + // Wait + wg.Wait() + + return nil +} diff --git a/lxc-to-lxd/network.go b/lxc-to-lxd/network.go new file mode 100644 index 000000000..97570064e --- /dev/null +++ b/lxc-to-lxd/network.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "strings" + + lxc "gopkg.in/lxc/go-lxc.v2" +) + +func networkGet(container *lxc.Container, index int, configKey string) map[string]string { + keys := container.ConfigKeys(fmt.Sprintf("%s.%d", configKey, index)) + if len(keys) == 0 { + return nil + } + + dev := make(map[string]string, 0) + for _, k := range keys { + value := container.ConfigItem(fmt.Sprintf("%s.%d.%s", configKey, index, k)) + if len(value) == 0 || strings.TrimSpace(value[0]) == "" { + continue + } + + dev[k] = value[0] + } + + if len(dev) == 0 { + return nil + } + + return dev +} diff --git a/lxc-to-lxd/transfer.go b/lxc-to-lxd/transfer.go new file mode 100644 index 000000000..c190d4d05 --- /dev/null +++ b/lxc-to-lxd/transfer.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "strings" + + "github.com/gorilla/websocket" + "github.com/pborman/uuid" + + "github.com/lxc/lxd/lxd/migration" + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/version" +) + +// Send an rsync stream of a path over a websocket +func rsyncSend(conn *websocket.Conn, path string, rsyncArgs string) error { + cmd, dataSocket, stderr, err := rsyncSendSetup(path, rsyncArgs) + if err != nil { + return err + } + + if dataSocket != nil { + defer dataSocket.Close() + } + + readDone, writeDone := shared.WebsocketMirror(conn, dataSocket, io.ReadCloser(dataSocket), nil, nil) + + output, err := ioutil.ReadAll(stderr) + if err != nil { + cmd.Process.Kill() + cmd.Wait() + return fmt.Errorf("Failed to rsync: %v\n%s", err, output) + } + + err = cmd.Wait() + <-readDone + <-writeDone + + if err != nil { + return fmt.Errorf("Failed to rsync: %v\n%s", err, output) + } + + return nil +} + +// Spawn the rsync process +func rsyncSendSetup(path string, rsyncArgs string) (*exec.Cmd, net.Conn, io.ReadCloser, error) { + auds := fmt.Sprintf("@lxc-to-lxd/%s", uuid.NewRandom().String()) + if len(auds) > shared.ABSTRACT_UNIX_SOCK_LEN-1 { + auds = auds[:shared.ABSTRACT_UNIX_SOCK_LEN-1] + } + + l, err := net.Listen("unix", auds) + if err != nil { + return nil, nil, nil, err + } + + execPath, err := os.Readlink("/proc/self/exe") + if err != nil { + return nil, nil, nil, err + } + + rsyncCmd := fmt.Sprintf("sh -c \"%s netcat %s\"", execPath, auds) + + args := []string{ + "-ar", + "--devices", + "--numeric-ids", + "--partial", + "--sparse", + } + + // Ignore deletions (requires 3.1 or higher) + rsyncCheckVersion := func(min string) bool { + out, err := shared.RunCommand("rsync", "--version") + if err != nil { + return false + } + + fields := strings.Split(out, " ") + curVer, err := version.Parse(fields[3]) + if err != nil { + return false + } + + minVer, err := version.Parse(min) + if err != nil { + return false + } + + return curVer.Compare(minVer) >= 0 + } + + if rsyncCheckVersion("3.1.0") { + args = append(args, "--ignore-missing-args") + } + + if rsyncArgs != "" { + args = append(args, strings.Split(rsyncArgs, " ")...) + } + + args = append(args, []string{path, "localhost:/tmp/foo"}...) + args = append(args, []string{"-e", rsyncCmd}...) + + cmd := exec.Command("rsync", args...) + cmd.Stdout = os.Stderr + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, nil, nil, err + } + + if err := cmd.Start(); err != nil { + return nil, nil, nil, err + } + + conn, err := l.Accept() + if err != nil { + cmd.Process.Kill() + cmd.Wait() + return nil, nil, nil, err + } + l.Close() + + return cmd, conn, stderr, nil +} + +func protoSendError(ws *websocket.Conn, err error) { + migration.ProtoSendControl(ws, err) + + if err != nil { + closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + ws.WriteMessage(websocket.CloseMessage, closeMsg) + ws.Close() + } +} diff --git a/lxc-to-lxd/utils.go b/lxc-to-lxd/utils.go new file mode 100644 index 000000000..bcad65d8b --- /dev/null +++ b/lxc-to-lxd/utils.go @@ -0,0 +1,182 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + "syscall" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/lxc/lxd/client" + "github.com/lxc/lxd/lxd/migration" + "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/api" +) + +func transferRootfs(dst lxd.ContainerServer, op lxd.Operation, rootfs string, rsyncArgs string) error { + opAPI := op.Get() + + // Connect to the websockets + wsControl, err := op.GetWebsocket(opAPI.Metadata["control"].(string)) + if err != nil { + return err + } + + wsFs, err := op.GetWebsocket(opAPI.Metadata["fs"].(string)) + if err != nil { + return err + } + + // Setup control struct + fs := migration.MigrationFSType_RSYNC + header := migration.MigrationHeader{ + Fs: &fs, + } + + err = migration.ProtoSend(wsControl, &header) + if err != nil { + protoSendError(wsControl, err) + return err + } + + err = migration.ProtoRecv(wsControl, &header) + if err != nil { + protoSendError(wsControl, err) + return err + } + + // Send the filesystem + abort := func(err error) error { + protoSendError(wsControl, err) + return err + } + + err = rsyncSend(wsFs, rootfs, rsyncArgs) + if err != nil { + return abort(err) + } + + // Check the result + msg := migration.MigrationControl{} + err = migration.ProtoRecv(wsControl, &msg) + if err != nil { + wsControl.Close() + return err + } + + if !*msg.Success { + return fmt.Errorf(*msg.Message) + } + + return nil +} + +func connectTarget(url string) (lxd.ContainerServer, error) { + // Generate a new client certificate for this + fmt.Println("Generating a temporary client certificate. This may take a minute...") + clientCrt, clientKey, err := shared.GenerateMemCert(true) + if err != nil { + return nil, err + } + + // Attempt to connect using the system CA + args := lxd.ConnectionArgs{} + args.TLSClientCert = string(clientCrt) + args.TLSClientKey = string(clientKey) + args.UserAgent = "LXC-TO-LXD" + c, err := lxd.ConnectLXD(url, &args) + + var certificate *x509.Certificate + if err != nil { + // Failed to connect using the system CA, so retrieve the remote certificate + certificate, err = shared.GetRemoteCertificate(url) + if err != nil { + return nil, err + } + } + + // Handle certificate prompt + if certificate != nil { + digest := shared.CertFingerprint(certificate) + + fmt.Printf("Certificate fingerprint: %s\n", digest) + fmt.Printf("ok (y/n)? ") + line, err := shared.ReadStdin() + if err != nil { + return nil, err + } + + if len(line) < 1 || line[0] != 'y' && line[0] != 'Y' { + return nil, fmt.Errorf("Server certificate rejected by user") + } + + serverCrt := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw}) + args.TLSServerCert = string(serverCrt) + + // Setup a new connection, this time with the remote certificate + c, err = lxd.ConnectLXD(url, &args) + if err != nil { + return nil, err + } + } + + // Get server information + srv, _, err := c.GetServer() + if err != nil { + return nil, err + } + + // Check if our cert is already trusted + if srv.Auth == "trusted" { + return c, nil + } + + // Prompt for trust password + fmt.Printf("Admin password for %s: ", url) + pwd, err := terminal.ReadPassword(0) + if err != nil { + return nil, err + } + fmt.Println("") + + // Add client certificate to trust store + req := api.CertificatesPost{ + Password: string(pwd), + } + req.Type = "client" + + err = c.CreateCertificate(req) + if err != nil { + return nil, err + } + + return c, nil +} + +func setupSource(path string, mounts []string) error { + prefix := "/" + if len(mounts) > 0 { + prefix = mounts[0] + } + + // Mount everything + for _, mount := range mounts { + target := fmt.Sprintf("%s/%s", path, strings.TrimPrefix(mount, prefix)) + + // Mount the path + err := syscall.Mount(mount, target, "none", syscall.MS_BIND, "") + if err != nil { + return fmt.Errorf("Failed to mount %s: %v", mount, err) + } + + // Make it read-only + err = syscall.Mount("", target, "none", syscall.MS_BIND|syscall.MS_RDONLY|syscall.MS_REMOUNT, "") + if err != nil { + return fmt.Errorf("Failed to make %s read-only: %v", mount, err) + } + } + + return nil +} From 9db8e09fd107c1b5e5433fb0a09522fc22bff7d3 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Wed, 27 Jun 2018 20:54:00 +0200 Subject: [PATCH 2/3] tests: Add lxc-to-lxd tests Signed-off-by: Thomas Hipp <[email protected]> --- lxc-to-lxd/main_migrate_test.go | 374 ++++++++++++++++++++++++++++++++++++++++ test/suites/lxc-to-lxd.sh | 60 +++++++ 2 files changed, 434 insertions(+) create mode 100644 lxc-to-lxd/main_migrate_test.go create mode 100644 test/suites/lxc-to-lxd.sh diff --git a/lxc-to-lxd/main_migrate_test.go b/lxc-to-lxd/main_migrate_test.go new file mode 100644 index 000000000..a0f5fbb02 --- /dev/null +++ b/lxc-to-lxd/main_migrate_test.go @@ -0,0 +1,374 @@ +package main + +import ( + "io/ioutil" + "log" + "os" + "strings" + "testing" + + "github.com/lxc/lxd/lxd/types" + "github.com/stretchr/testify/require" + lxc "gopkg.in/lxc/go-lxc.v2" +) + +func TestValidateConfig(t *testing.T) { + tests := []struct { + name string + config []string + err string + shouldFail bool + }{ + { + "container migrated", + []string{ + "lxd.migrated = 1", + }, + "Container has already been migrated", + true, + }, + { + "container name missmatch (1)", + []string{ + "lxc.uts.name = c2", + }, + "Container name doesn't match lxc.uts.name / lxc.utsname", + true, + }, + { + "container name missmatch (2)", + []string{ + "lxc.utsname = c2", + }, + "Container name doesn't match lxc.uts.name / lxc.utsname", + true, + }, + { + "incomplete AppArmor support (1)", + []string{ + "lxc.uts.name = c1", + "lxc.apparmor.allow_incomplete = 1", + }, + "Container allows incomplete AppArmor support", + true, + }, + { + "incomplete AppArmor support (2)", + []string{ + "lxc.uts.name = c1", + "lxc.aa_allow_incomplete = 1", + }, + "Container allows incomplete AppArmor support", + true, + }, + { + "missing minimal /dev filesystem", + []string{ + "lxc.uts.name = c1", + "lxc.apparmor.allow_incomplete = 0", + "lxc.autodev = 0", + }, + "Container doesn't mount a minimal /dev filesystem", + true, + }, + { + "missing lxc.rootfs key", + []string{ + "lxc.uts.name = c1", + "lxc.apparmor.allow_incomplete = 0", + "lxc.autodev = 1", + }, + "Invalid container, missing lxc.rootfs key", + true, + }, + { + "invalid lxc.rootfs key", + []string{ + "lxc.uts.name = c1", + "lxc.apparmor.allow_incomplete = 0", + "lxc.autodev = 1", + "lxc.rootfs = /invalid/path", + }, + "Invalid container, invalid lxc.rootfs key", + true, + }, + { + "non-existent rootfs path", + []string{ + "lxc.uts.name = c1", + "lxc.apparmor.allow_incomplete = 0", + "lxc.autodev = 1", + "lxc.rootfs = dir:/invalid/path", + }, + "Couldn't find the container rootfs '/invalid/path'", + true, + }, + } + + lxcPath, err := ioutil.TempDir("", "lxc-to-lxd-test-") + require.NoError(t, err) + defer os.RemoveAll(lxcPath) + + c, err := lxc.NewContainer("c1", lxcPath) + require.NoError(t, err) + + for i, tt := range tests { + log.Printf("Running test #%d: %s", i, tt.name) + err := validateConfig(tt.config, c) + if tt.shouldFail { + require.EqualError(t, err, tt.err) + } else { + require.NoError(t, err) + } + } +} + +func TestConvertNetworkConfig(t *testing.T) { + tests := []struct { + name string + config []string + expectedDevices types.Devices + expectedError string + shouldFail bool + }{ + { + "loopback only", + []string{}, + types.Devices{ + "eth0": map[string]string{ + "type": "none", + }, + }, + "", + false, + }, + { + "multiple network devices", + []string{ + "lxc.net.1.type = macvlan", + "lxc.net.1.macvlan.mode = bridge", + "lxc.net.1.link = mvlan0", + "lxc.net.1.hwaddr = 00:16:3e:8d:4f:51", + "lxc.net.1.name = eth1", + "lxc.net.2.type = veth", + "lxc.net.2.link = lxcbr0", + "lxc.net.2.hwaddr = 00:16:3e:a2:7d:54", + "lxc.net.2.name = eth2", + }, + types.Devices{ + "convert_net2": map[string]string{ + "type": "nic", + "nictype": "bridged", + "parent": "lxcbr0", + "name": "eth2", + "hwaddr": "00:16:3e:a2:7d:54", + }, + "eth0": map[string]string{ + "type": "none", + }, + "convert_net1": map[string]string{ + "name": "eth1", + "hwaddr": "00:16:3e:8d:4f:51", + "type": "nic", + "nictype": "macvlan", + "parent": "mvlan0", + }, + }, + "", + false, + }, + } + + lxcPath, err := ioutil.TempDir("", "lxc-to-lxd-test-") + require.NoError(t, err) + defer os.RemoveAll(lxcPath) + + for i, tt := range tests { + log.Printf("Running test #%d: %s", i, tt.name) + + c, err := lxc.NewContainer("c1", lxcPath) + require.NoError(t, err) + + err = c.Create(lxc.TemplateOptions{Template: "busybox"}) + require.NoError(t, err) + + for _, conf := range tt.config { + parts := strings.SplitN(conf, "=", 2) + require.Equal(t, 2, len(parts)) + err := c.SetConfigItem(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) + require.NoError(t, err) + } + + devices := make(types.Devices, 0) + err = convertNetworkConfig(c, devices) + if tt.shouldFail { + require.EqualError(t, err, tt.expectedError) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedDevices, devices) + } + + err = c.Destroy() + require.NoError(t, err) + } +} + +func TestConvertStorageConfig(t *testing.T) { + tests := []struct { + name string + config []string + expectedDevices types.Devices + expectedError string + shouldFail bool + }{ + { + "invalid path", + []string{ + "lxc.mount.entry = /foo lib none ro,bind 0 0", + }, + types.Devices{}, + "Invalid path: /foo", + true, + }, + { + "invalid rootfs", + []string{ + "lxc.rootfs.path = /invalid", + "lxc.mount.entry = /lib /lib none ro,bind 0 0", + }, + types.Devices{}, + "Invalid container, invalid lxc.rootfs key", + true, + }, + { + "ignored default mounts", + []string{ + "lxc.mount.entry = proc /proc proc defaults 0 0", + }, + types.Devices{}, + "", + false, + }, + { + "ignored mounts", + []string{ + "lxc.mount.entry = shm /dev/shm tmpfs defaults 0 0", + }, + types.Devices{}, + "", + false, + }, + { + "valid mount configuration", + []string{ + "lxc.rootfs.path = dir:/tmp", + "lxc.mount.entry = /lib lib none ro,bind 0 0", + "lxc.mount.entry = /usr/lib usr/lib none ro,bind 1 0", + "lxc.mount.entry = /lib64 lib64 none ro,bind 0 0", + "lxc.mount.entry = /usr/lib64 usr/lib64 none ro,bind 1 0", + "lxc.mount.entry = /sys/kernel/security /sys/kernel/security none ro,bind,optional 1 0", + "lxc.mount.entry = /mnt /tmp/mnt none ro,bind 0 0", + }, + types.Devices{ + "convert_mount0": map[string]string{ + "type": "disk", + "readonly": "true", + "source": "/lib", + "path": "/lib", + }, + "convert_mount1": map[string]string{ + "type": "disk", + "readonly": "true", + "source": "/usr/lib", + "path": "/usr/lib", + }, + "convert_mount2": map[string]string{ + "type": "disk", + "readonly": "true", + "source": "/lib64", + "path": "/lib64", + }, + "convert_mount3": map[string]string{ + "type": "disk", + "readonly": "true", + "source": "/usr/lib64", + "path": "/usr/lib64", + }, + "convert_mount4": map[string]string{ + "type": "disk", + "readonly": "true", + "optional": "true", + "source": "/sys/kernel/security", + "path": "/sys/kernel/security", + }, + "convert_mount5": map[string]string{ + "type": "disk", + "readonly": "true", + "source": "/mnt", + "path": "/mnt", + }, + }, + "", + false, + }, + } + + for i, tt := range tests { + log.Printf("Running test #%d: %s", i, tt.name) + devices := make(types.Devices, 0) + err := convertStorageConfig(tt.config, devices) + if tt.shouldFail { + require.EqualError(t, err, tt.expectedError) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedDevices, devices) + } + } +} + +func TestGetRootfs(t *testing.T) { + tests := []struct { + name string + config []string + expectedOutput string + expectedError string + shouldFail bool + }{ + { + "missing lxc.rootfs key", + []string{}, + "", + "Invalid container, missing lxc.rootfs key", + true, + }, + { + "invalid lxc.rootfs key", + []string{ + "lxc.rootfs = foobar", + }, + "", + "Invalid container, invalid lxc.rootfs key", + true, + }, + { + "valid lxc.rootfs key", + []string{ + "lxc.rootfs = dir:foobar", + }, + "foobar", + "", + false, + }, + } + + for i, tt := range tests { + log.Printf("Running test #%d: %s", i, tt.name) + rootfs, err := getRootfs(tt.config) + require.Equal(t, tt.expectedOutput, rootfs) + if tt.shouldFail { + require.EqualError(t, err, tt.expectedError) + } else { + require.NoError(t, err) + } + } +} diff --git a/test/suites/lxc-to-lxd.sh b/test/suites/lxc-to-lxd.sh new file mode 100644 index 000000000..238c7e7ef --- /dev/null +++ b/test/suites/lxc-to-lxd.sh @@ -0,0 +1,60 @@ +test_lxc_to_lxd() { + ensure_has_localhost_remote "${LXD_ADDR}" + + LXC_DIR="${TEST_DIR}/lxc" + + mkdir -p "${LXC_DIR}" + + # Create LXC containers + lxc-create -P "${LXC_DIR}" -n c1 -B dir -t busybox + lxc-create -P "${LXC_DIR}" -n c2 -B dir -t busybox + lxc-create -P "${LXC_DIR}" -n c3 -B dir -t busybox + + # Convert single LXC container (dry run) + lxc-to-lxd --lxcpath "${LXC_DIR}" --dry-run --delete --containers c1 + + # Ensure the LXC containers have not been deleted + [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 3 ]] + + # Ensure no containers have been converted + ! lxc info c1 + ! lxc info c2 + ! lxc info c3 + + # Convert single LXC container + lxc-to-lxd --lxcpath "${LXC_DIR}" --containers c1 + + # Ensure the LXC containers have not been deleted + [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 3 ]] + + # Ensure only c1 has been converted + lxc info c1 + ! lxc info c2 + ! lxc info c3 + + # Ensure the converted container is startable + lxc start c1 + lxc delete -f c1 + + # Convert some LXC containers + lxc-to-lxd --lxcpath "${LXC_DIR}" --delete --containers c1,c2 + + # Ensure the LXC containers c1 and c2 have been deleted + [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 1 ]] + + # Ensure all containers have been converted + lxc info c1 + lxc info c2 + ! lxc info c3 + + # Convert all LXC containers + lxc-to-lxd --lxcpath "${LXC_DIR}" --delete --all + + # Ensure the remaining LXC containers have been deleted + [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 0 ]] + + # Ensure all containers have been converted + lxc info c1 + lxc info c2 + lxc info c3 +} From 85e45554ccc180a55c13b213ec3c9e0019344722 Mon Sep 17 00:00:00 2001 From: Thomas Hipp <[email protected]> Date: Tue, 3 Jul 2018 15:16:44 +0200 Subject: [PATCH 3/3] scripts: Remove lxc-to-lxd Signed-off-by: Thomas Hipp <[email protected]> --- scripts/lxc-to-lxd | 641 ----------------------------------------------------- 1 file changed, 641 deletions(-) delete mode 100755 scripts/lxc-to-lxd diff --git a/scripts/lxc-to-lxd b/scripts/lxc-to-lxd deleted file mode 100755 index 4a93a4e16..000000000 --- a/scripts/lxc-to-lxd +++ /dev/null @@ -1,641 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import http.client -import json -import os -import socket -import subprocess -import sys - -try: - import lxc -except ImportError: - print("You must have python3-lxc installed for this script to work.") - sys.exit(1) - - -# Whitelist of keys we either need to check or allow setting in LXD. The latter -# is strictly only true for 'lxc.aa_profile'. -keys_to_check = [ - 'lxc.pts', - # 'lxc.tty', - # 'lxc.devttydir', - # 'lxc.kmsg', - 'lxc.aa_profile', - # 'lxc.cgroup.', - 'lxc.loglevel', - # 'lxc.logfile', - 'lxc.mount.auto', - 'lxc.mount', - # 'lxc.rootfs.mount', - # 'lxc.rootfs.options', - # 'lxc.pivotdir', - # 'lxc.hook.pre-start', - # 'lxc.hook.pre-mount', - # 'lxc.hook.mount', - # 'lxc.hook.autodev', - # 'lxc.hook.start', - # 'lxc.hook.stop', - # 'lxc.hook.post-stop', - # 'lxc.hook.clone', - # 'lxc.hook.destroy', - # 'lxc.hook', - 'lxc.network.type', - 'lxc.network.flags', - 'lxc.network.link', - 'lxc.network.name', - 'lxc.network.macvlan.mode', - 'lxc.network.veth.pair', - # 'lxc.network.script.up', - # 'lxc.network.script.down', - 'lxc.network.hwaddr', - 'lxc.network.mtu', - # 'lxc.network.vlan.id', - # 'lxc.network.ipv4.gateway', - # 'lxc.network.ipv4', - # 'lxc.network.ipv6.gateway', - # 'lxc.network.ipv6', - # 'lxc.network.', - # 'lxc.network', - # 'lxc.console.logfile', - # 'lxc.console', - 'lxc.include', - 'lxc.start.auto', - 'lxc.start.delay', - 'lxc.start.order', - # 'lxc.monitor.unshare', - # 'lxc.group', - 'lxc.environment', - # 'lxc.init_cmd', - # 'lxc.init_uid', - # 'lxc.init_gid', - # 'lxc.ephemeral', - # 'lxc.syslog', - # 'lxc.no_new_privs', - - # Additional keys that are either set by this script or are used to report - # helpful errors to users. - 'lxc.arch', - 'lxc.id_map', - 'lxd.migrated', - 'lxc.rootfs.backend', - 'lxc.rootfs', - 'lxc.utsname', - 'lxc.aa_allow_incomplete', - 'lxc.autodev', - 'lxc.haltsignal', - 'lxc.rebootsignal', - 'lxc.stopsignal', - 'lxc.mount.entry', - 'lxc.cap.drop', - # 'lxc.cap.keep', - 'lxc.seccomp', - # 'lxc.se_context', - ] - - -# Unix connection to LXD -class UnixHTTPConnection(http.client.HTTPConnection): - def __init__(self, path): - http.client.HTTPConnection.__init__(self, 'localhost') - self.path = path - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock - - -# Fetch a config key as a list -def config_get(config, key, default=None): - result = [] - for line in config: - fields = line.split("=", 1) - if fields[0].strip() == key: - result.append(fields[-1].strip()) - - if len(result) == 0: - return default - else: - return result - - -def config_keys(config): - keys = [] - for line in config: - fields = line.split("=", 1) - cur = fields[0].strip() - if cur and not cur.startswith("#") and cur.startswith("lxc."): - keys.append(cur) - - return keys - - -# Parse a LXC configuration file, called recursively for includes -def config_parse(path): - config = [] - with open(path, "r") as fd: - for line in fd: - line = line.strip() - key = line.split("=", 1)[0].strip() - value = line.split("=", 1)[-1].strip() - - # Parse user-added includes - if key == "lxc.include": - # Ignore our own default configs - if value.startswith("/usr/share/lxc/config/"): - continue - - if os.path.isfile(value): - config += config_parse(value) - continue - elif os.path.isdir(value): - for entry in os.listdir(value): - if not entry.endswith(".conf"): - continue - - config += config_parse(os.path.join(value, entry)) - continue - else: - print("Invalid include: %s", line) - - # Expand any fstab - if key == "lxc.mount": - if not os.path.exists(value): - print("Container fstab file doesn't exist, skipping...") - continue - - with open(value, "r") as fd: - for line in fd: - line = line.strip() - if (line and not line.startswith("#") and - line.startswith("lxc.")): - config.append("lxc.mount.entry = %s" % line) - continue - - # Process normal configuration keys - if line and not line.strip().startswith("#"): - config.append(line) - - return config - - -def container_exists(lxd_socket, container_name): - lxd = UnixHTTPConnection(lxd_socket) - lxd.request("GET", "/1.0/containers/%s" % container_name) - if lxd.getresponse().status == 404: - return False - - return True - - -def container_create(lxd_socket, args): - # Define the container - lxd = UnixHTTPConnection(lxd_socket) - lxd.request("POST", "/1.0/containers", json.dumps(args)) - r = lxd.getresponse() - - # Decode the response - resp = json.loads(r.read().decode()) - if resp["type"] == "error": - raise Exception("Failed to define container: %s" % resp["error"]) - - # Wait for result - lxd = UnixHTTPConnection(lxd_socket) - lxd.request("GET", "%s/wait" % resp["operation"]) - r = lxd.getresponse() - - # Decode the response - resp = json.loads(r.read().decode()) - if resp["type"] == "error": - raise Exception("Failed to define container: %s" % resp["error"]) - - -# Convert a LXC container to a LXD one -def convert_container(lxd_socket, container_name, args): - print("==> Processing container: %s" % container_name) - - # Load the container - try: - container = lxc.Container(container_name, args.lxcpath) - except Exception: - print("Invalid container configuration, skipping...") - return False - - # As some keys can't be queried over the API, parse the config ourselves - print("Parsing LXC configuration") - lxc_config = config_parse(container.config_file_name) - found_keys = config_keys(lxc_config) - - # Generic check for any invalid LXC configuration keys. - print("Checking for unsupported LXC configuration keys") - diff = list(set(found_keys) - set(keys_to_check)) - for d in diff: - if (not d.startswith('lxc.network.') and not - d.startswith('lxc.cgroup.')): - print("Found at least one unsupported config key %s: " % d) - print("Not importing this container, skipping...") - return False - - if args.debug: - print("Container configuration:") - print(" ", end="") - print("\n ".join(lxc_config)) - print("") - - # Check for keys that have values differing from the LXD defaults. - print("Checking whether container has already been migrated") - if config_get(lxc_config, "lxd.migrated"): - print("Container has already been migrated, skipping...") - return False - - # Make sure we don't have a conflict - print("Checking for existing containers") - if container_exists(lxd_socket, container_name): - print("Container already exists, skipping...") - return False - - # Validating lxc.id_map: must be unset. - print("Validating container mode") - if config_get(lxc_config, "lxc.id_map"): - print("Unprivileged containers aren't supported, skipping...") - return False - - # Validate lxc.utsname - print("Validating container name") - value = config_get(lxc_config, "lxc.utsname") - if value and value[0] != container_name: - print("Container name doesn't match lxc.utsname, skipping...") - return False - - # Validate lxc.aa_allow_incomplete: must be set to 0 or unset. - print("Validating whether incomplete AppArmor support is enabled") - value = config_get(lxc_config, "lxc.aa_allow_incomplete") - if value and int(value[0]) != 0: - print("Container allows incomplete AppArmor support, skipping...") - return False - - # Validate lxc.autodev: must be set to 1 or unset. - print("Validating whether mounting a minimal /dev is enabled") - value = config_get(lxc_config, "lxc.autodev") - if value and int(value[0]) != 1: - print("Container doesn't mount a minimal /dev filesystem, skipping...") - return False - - # Validate lxc.haltsignal: must be unset. - print("Validating that no custom haltsignal is set") - value = config_get(lxc_config, "lxc.haltsignal") - if value: - print("Container sets custom halt signal, skipping...") - return False - - # Validate lxc.rebootsignal: must be unset. - print("Validating that no custom rebootsignal is set") - value = config_get(lxc_config, "lxc.rebootsignal") - if value: - print("Container sets custom reboot signal, skipping...") - return False - - # Validate lxc.stopsignal: must be unset. - print("Validating that no custom stopsignal is set") - value = config_get(lxc_config, "lxc.stopsignal") - if value: - print("Container sets custom stop signal, skipping...") - return False - - # Extract and valid rootfs key - print("Validating container rootfs") - value = config_get(lxc_config, "lxc.rootfs") - if not value: - print("Invalid container, missing lxc.rootfs key, skipping...") - return False - - rootfs = value[0] - - if not os.path.exists(rootfs): - print("Couldn't find the container rootfs '%s', skipping..." % rootfs) - return False - - # Base config - config = {} - config['security.privileged'] = "true" - devices = {} - devices['eth0'] = {'type': "none"} - - # Convert network configuration - print("Processing network configuration") - try: - count = len(container.get_config_item("lxc.network")) - except Exception: - count = 0 - - for i in range(count): - device = {"type": "nic"} - - # Get the device type - device["nictype"] = container.get_config_item("lxc.network")[i] - - # Get everything else - dev = container.network[i] - - # Validate configuration - if dev.ipv4 or dev.ipv4_gateway: - print("IPv4 network configuration isn't supported, skipping...") - return False - - if dev.ipv6 or dev.ipv6_gateway: - print("IPv6 network configuration isn't supported, skipping...") - return False - - if dev.script_up or dev.script_down: - print("Network config scripts aren't supported, skipping...") - return False - - if device["nictype"] == "none": - print("\"none\" network mode isn't supported, skipping...") - return False - - if device["nictype"] == "vlan": - print("\"vlan\" network mode isn't supported, skipping...") - return False - - # Convert the configuration - if dev.hwaddr: - device['hwaddr'] = dev.hwaddr - - if dev.link: - device['parent'] = dev.link - - if dev.mtu: - device['mtu'] = dev.mtu - - if dev.name: - device['name'] = dev.name - - if dev.veth_pair: - device['host_name'] = dev.veth_pair - - if device["nictype"] == "veth": - if "parent" in device: - device["nictype"] = "bridged" - else: - device["nictype"] = "p2p" - - if device["nictype"] == "phys": - device["nictype"] = "physical" - - if device["nictype"] == "empty": - continue - - devices['convert_net%d' % i] = device - count += 1 - - # Convert storage configuration - value = config_get(lxc_config, "lxc.mount.entry", []) - i = 0 - for entry in value: - mount = entry.split(" ") - if len(mount) < 4: - print("Invalid mount configuration, skipping...") - return False - - # Ignore mounts that are present in LXD containers by default. - if mount[0] in ("proc", "sysfs"): - continue - - device = {'type': "disk"} - - # Deal with read-only mounts - if "ro" in mount[3].split(","): - device['readonly'] = "true" - - # Deal with optional mounts - if "optional" in mount[3].split(","): - device['optional'] = "true" - elif not os.path.exists(mount[0]): - print("Invalid mount configuration, source path doesn't exist.") - return False - - # Set the source - device['source'] = mount[0] - - # Figure out the target - if mount[1][0] != "/": - device['path'] = "/%s" % mount[1] - else: - device['path'] = mount[1].split(rootfs, 1)[-1] - - devices['convert_mount%d' % i] = device - i += 1 - - # Convert environment - print("Processing environment configuration") - value = config_get(lxc_config, "lxc.environment", []) - for env in value: - entry = env.split("=", 1) - config['environment.%s' % entry[0].strip()] = entry[-1].strip() - - # Convert auto-start - print("Processing container boot configuration") - value = config_get(lxc_config, "lxc.start.auto") - if value and int(value[0]) > 0: - config['boot.autostart'] = "true" - - value = config_get(lxc_config, "lxc.start.delay") - if value and int(value[0]) > 0: - config['boot.autostart.delay'] = value[0] - - value = config_get(lxc_config, "lxc.start.order") - if value and int(value[0]) > 0: - config['boot.autostart.priority'] = value[0] - - # Convert apparmor - print("Processing container apparmor configuration") - value = config_get(lxc_config, "lxc.aa_profile") - if value: - if value[0] == "lxc-container-default-with-nesting": - config['security.nesting'] = "true" - elif value[0] != "lxc-container-default": - config["raw.lxc"] = "lxc.aa_profile=%s" % value[0] - - # Convert seccomp - print("Processing container seccomp configuration") - value = config_get(lxc_config, "lxc.seccomp") - if value and value[0] != "/usr/share/lxc/config/common.seccomp": - print("Custom seccomp profiles aren't supported, skipping...") - return False - - # Convert SELinux - print("Processing container SELinux configuration") - value = config_get(lxc_config, "lxc.se_context") - if value: - print("Custom SELinux policies aren't supported, skipping...") - return False - - # Convert capabilities - print("Processing container capabilities configuration") - value = config_get(lxc_config, "lxc.cap.drop") - if value: - for cap in value: - # Ignore capabilities that are dropped in LXD containers by default. - if cap in ("mac_admin", "mac_override", "sys_module", "sys_time"): - continue - print("Custom capabilities aren't supported, skipping...") - return False - - value = config_get(lxc_config, "lxc.cap.keep") - if value: - print("Custom capabilities aren't supported, skipping...") - return False - - # Setup the container creation request - new = {'name': container_name, - 'source': {'type': 'none'}, - 'config': config, - 'devices': devices, - 'profiles': ["default"]} - - # Set the container architecture if set in LXC - print("Processing container architecture configuration") - arches = {'i686': "i686", - 'x86_64': "x86_64", - 'armhf': "armv7l", - 'arm64': "aarch64", - 'powerpc': "ppc", - 'powerpc64': "ppc64", - 'ppc64el': "ppc64le", - 's390x': "s390x"} - - arch = None - try: - arch = config_get(lxc_config, "lxc.arch", None) - - if arch and arch[0] in arches: - new['architecture'] = arches[arch[0]] - else: - print("Unknown architecture, assuming native.") - except Exception: - print("Couldn't find container architecture, assuming native.") - - # Define the container in LXD - if args.debug: - print("LXD container config:") - print(json.dumps(new, indent=True, sort_keys=True)) - - if args.dry_run: - return True - - if container.running: - print("Only stopped containers can be migrated, skipping...") - return False - - try: - print("Creating the container") - container_create(lxd_socket, new) - except Exception as e: - raise - print("Failed to create the container: %s" % e) - return False - - # Transfer the filesystem - lxd_rootfs = os.path.join(args.lxdpath, "containers", - container_name, "rootfs") - - if args.move_rootfs: - if os.path.exists(lxd_rootfs): - os.rmdir(lxd_rootfs) - - if subprocess.call(["mv", rootfs, lxd_rootfs]) != 0: - print("Failed to move the container rootfs, skipping...") - return False - - os.mkdir(rootfs) - else: - print("Copying container rootfs") - if not os.path.exists(lxd_rootfs): - os.mkdir(lxd_rootfs) - - if subprocess.call(["rsync", "-Aa", "--sparse", - "--acls", "--numeric-ids", "--hard-links", - "%s/" % rootfs, "%s/" % lxd_rootfs]) != 0: - print("Failed to transfer the container rootfs, skipping...") - return False - - # Delete the source - if args.delete: - print("Deleting source container") - container.delete() - - # Mark the container as migrated - with open(container.config_file_name, "a") as fd: - fd.write("lxd.migrated=true\n") - print("Container is ready to use") - return True - - -# Argument parsing -parser = argparse.ArgumentParser() -parser.add_argument("--dry-run", action="store_true", default=False, - help="Dry run mode") -parser.add_argument("--debug", action="store_true", default=False, - help="Print debugging output") -parser.add_argument("--all", action="store_true", default=False, - help="Import all containers") -parser.add_argument("--delete", action="store_true", default=False, - help="Delete the source container") -parser.add_argument("--move-rootfs", action="store_true", default=False, - help="Move the container rootfs rather than copying it") -parser.add_argument("--lxcpath", type=str, default=False, - help="Alternate LXC path") -parser.add_argument("--lxdpath", type=str, default="/var/lib/lxd", - help="Alternate LXD path") -parser.add_argument(dest='containers', metavar="CONTAINER", type=str, - help="Container to import", nargs="*") -args = parser.parse_args() - -# Sanity checks -if not os.geteuid() == 0: - parser.error("You must be root to run this tool") - -if (not args.containers and not args.all) or (args.containers and args.all): - parser.error("You must either pass container names or --all") - -# Connect to LXD -if 'LXD_SOCKET' in os.environ: - lxd_socket = os.environ['LXD_SOCKET'] -else: - lxd_socket = os.path.join(args.lxdpath, "unix.socket") - -if not os.path.exists(lxd_socket): - print("LXD isn't running.") - sys.exit(1) - -# Run migration -results = {} -count = 0 -for container_name in lxc.list_containers(config_path=args.lxcpath): - if args.containers and container_name not in args.containers: - continue - - if count > 0: - print("") - - results[container_name] = convert_container(lxd_socket, - container_name, args) - count += 1 - -# Print summary -if not results: - print("No container to migrate") - sys.exit(0) - -print("") -print("==> Migration summary") -for name, result in results.items(): - if result: - print("%s: SUCCESS" % name) - else: - print("%s: FAILURE" % name) - -if False in results.values(): - sys.exit(1)
_______________________________________________ lxc-devel mailing list [email protected] http://lists.linuxcontainers.org/listinfo/lxc-devel
