func validateConfig(c container, devs shared.Devices) error { for _, dev := range devs { if dev["type"] == "disk" && shared.IsBlockdevPath(dev["source"]) { if !c.IsPrivileged() { return fmt.Errorf("Only privileged containers may mount block devices") } } } return nil }
func deviceGetParentBlocks(path string) ([]string, error) { var devices []string // Expand the mount path absPath, err := filepath.Abs(path) if err != nil { return nil, err } expPath, err := filepath.EvalSymlinks(absPath) if err != nil { expPath = absPath } // Find the source mount of the path file, err := os.Open("/proc/self/mountinfo") if err != nil { return nil, err } defer file.Close() scanner := bufio.NewScanner(file) device := "" match := "" for scanner.Scan() { line := scanner.Text() rows := strings.Fields(line) if len(rows[4]) <= len(match) { continue } if expPath != rows[4] && !strings.HasPrefix(expPath, rows[4]) { continue } match = rows[4] // Go backward to avoid problems with optional fields device = rows[len(rows)-2] } if device == "" { return nil, fmt.Errorf("Couldn't find a match /proc/self/mountinfo entry") } // Deal with per-filesystem oddities. We don't care about failures here // because any non-special filesystem => directory backend. fs, _ := filesystemDetect(expPath) if fs == "zfs" && shared.PathExists("/dev/zfs") { poolName := strings.Split(device, "/")[0] output, err := exec.Command("zpool", "status", poolName).CombinedOutput() if err != nil { return nil, fmt.Errorf("Failed to query zfs filesystem information for %s: %s", device, output) } for _, line := range strings.Split(string(output), "\n") { fields := strings.Fields(line) if len(fields) < 5 { continue } if fields[1] != "ONLINE" { continue } if shared.PathExists(fields[0]) { if shared.IsBlockdevPath(fields[0]) { devices = append(devices, fields[0]) } else { subDevices, err := deviceGetParentBlocks(fields[0]) if err != nil { return nil, err } for _, dev := range subDevices { devices = append(devices, dev) } } } else if shared.PathExists(fmt.Sprintf("/dev/%s", fields[0])) { devices = append(devices, fmt.Sprintf("/dev/%s", fields[0])) } else if shared.PathExists(fmt.Sprintf("/dev/disk/by-id/%s", fields[0])) { devices = append(devices, fmt.Sprintf("/dev/disk/by-id/%s", fields[0])) } else { continue } } } else if fs == "btrfs" && shared.PathExists(device) { output, err := exec.Command("btrfs", "filesystem", "show", device).CombinedOutput() if err != nil { return nil, fmt.Errorf("Failed to query btrfs filesystem information for %s: %s", device, output) } for _, line := range strings.Split(string(output), "\n") { fields := strings.Fields(line) if len(fields) == 0 || fields[0] != "devid" { continue } devices = append(devices, fields[len(fields)-1]) } } else if shared.PathExists(device) { devices = append(devices, device) } // Expand the device paths for i, dev := range devices { target, err := filepath.EvalSymlinks(dev) if err == nil { devices[i] = target } } return devices, nil }
func deviceToLxc(d shared.Device) ([][]string, error) { switch d["type"] { case "unix-char": return nil, fmt.Errorf("Not implemented") case "unix-block": return nil, fmt.Errorf("Not implemented") case "nic": if d["nictype"] != "bridged" && d["nictype"] != "" { return nil, fmt.Errorf("Bad nic type: %s\n", d["nictype"]) } var l1 = []string{"lxc.network.type", "veth"} var lines = [][]string{l1} var l2 []string if d["hwaddr"] != "" { l2 = []string{"lxc.network.hwaddr", d["hwaddr"]} lines = append(lines, l2) } if d["mtu"] != "" { l2 = []string{"lxc.network.mtu", d["mtu"]} lines = append(lines, l2) } if d["parent"] != "" { l2 = []string{"lxc.network.link", d["parent"]} lines = append(lines, l2) } if d["name"] != "" { l2 = []string{"lxc.network.name", d["name"]} lines = append(lines, l2) } return lines, nil case "disk": var p string configLines := [][]string{} if d["path"] == "/" || d["path"] == "" { p = "" } else if d["path"][0:1] == "/" { p = d["path"][1:] } else { p = d["path"] } /* TODO - check whether source is a disk, loopback, btrfs subvol, etc */ /* for now we only handle directory bind mounts */ source := d["source"] fstype := "none" options := []string{} var err error if shared.IsBlockdevPath(d["source"]) { fstype, err = shared.BlockFsDetect(d["source"]) if err != nil { return nil, fmt.Errorf("Error setting up %s: %s\n", d["name"], err) } l, err := addBlockDev(d["source"]) if err != nil { return nil, fmt.Errorf("Error adding blockdev: %s\n", err) } configLines = append(configLines, l) } else if shared.IsDir(source) { options = append(options, "bind") options = append(options, "create=dir") } else /* file bind mount */ { /* Todo - can we distinguish between file bind mount and * a qcow2 (or other fs container) file? */ options = append(options, "bind") options = append(options, "create=file") } if d["readonly"] == "1" || d["readonly"] == "true" { options = append(options, "ro") } if d["optional"] == "1" || d["optional"] == "true" { options = append(options, "optional") } opts := strings.Join(options, ",") if opts == "" { opts = "defaults" } l := []string{"lxc.mount.entry", fmt.Sprintf("%s %s %s %s 0 0", source, p, fstype, opts)} configLines = append(configLines, l) return configLines, nil case "none": return nil, nil default: return nil, fmt.Errorf("Bad device type") } }
func cmdInit() error { var defaultPrivileged int // controls whether we set security.privileged=true var storageBackend string // dir or zfs var storageMode string // existing, loop or device var storageLoopSize int64 // Size in GB var storageDevice string // Path var storagePool string // pool name var networkAddress string // Address var networkPort int64 // Port var trustPassword string // Trust password var imagesAutoUpdate bool // controls whether we set images.auto_update_interval to 0 var bridgeName string // Bridge name var bridgeIPv4 string // IPv4 address var bridgeIPv4Nat bool // IPv4 address var bridgeIPv6 string // IPv6 address var bridgeIPv6Nat bool // IPv6 address // Detect userns defaultPrivileged = -1 runningInUserns = shared.RunningInUserNS() imagesAutoUpdate = true // Only root should run this if os.Geteuid() != 0 { return fmt.Errorf("This must be run as root") } backendsAvailable := []string{"dir"} backendsSupported := []string{"dir", "zfs"} // Detect zfs out, err := exec.LookPath("zfs") if err == nil && len(out) != 0 && !runningInUserns { _ = loadModule("zfs") err := shared.RunCommand("zpool", "list") if err == nil { backendsAvailable = append(backendsAvailable, "zfs") } } reader := bufio.NewReader(os.Stdin) askBool := func(question string, default_ string) bool { for { fmt.Printf(question) input, _ := reader.ReadString('\n') input = strings.TrimSuffix(input, "\n") if input == "" { input = default_ } if shared.StringInSlice(strings.ToLower(input), []string{"yes", "y"}) { return true } else if shared.StringInSlice(strings.ToLower(input), []string{"no", "n"}) { return false } fmt.Printf("Invalid input, try again.\n\n") } } askChoice := func(question string, choices []string, default_ string) string { for { fmt.Printf(question) input, _ := reader.ReadString('\n') input = strings.TrimSuffix(input, "\n") if input == "" { input = default_ } if shared.StringInSlice(input, choices) { return input } fmt.Printf("Invalid input, try again.\n\n") } } askInt := func(question string, min int64, max int64, default_ string) int64 { for { fmt.Printf(question) input, _ := reader.ReadString('\n') input = strings.TrimSuffix(input, "\n") if input == "" { input = default_ } intInput, err := strconv.ParseInt(input, 10, 64) if err == nil && (min == -1 || intInput >= min) && (max == -1 || intInput <= max) { return intInput } fmt.Printf("Invalid input, try again.\n\n") } } askString := func(question string, default_ string, validate func(string) error) string { for { fmt.Printf(question) input, _ := reader.ReadString('\n') input = strings.TrimSuffix(input, "\n") if input == "" { input = default_ } if validate != nil { result := validate(input) if result != nil { fmt.Printf("Invalid input: %s\n\n", result) continue } } if len(input) != 0 { return input } fmt.Printf("Invalid input, try again.\n\n") } } askPassword := func(question string) string { for { fmt.Printf(question) pwd, _ := terminal.ReadPassword(0) fmt.Printf("\n") inFirst := string(pwd) inFirst = strings.TrimSuffix(inFirst, "\n") fmt.Printf("Again: ") pwd, _ = terminal.ReadPassword(0) fmt.Printf("\n") inSecond := string(pwd) inSecond = strings.TrimSuffix(inSecond, "\n") if inFirst == inSecond { return inFirst } fmt.Printf("Invalid input, try again.\n\n") } } // Confirm that LXD is online c, err := lxd.NewClient(&lxd.DefaultConfig, "local") if err != nil { return fmt.Errorf("Unable to talk to LXD: %s", err) } // Check that we have no containers or images in the store containers, err := c.ListContainers() if err != nil { return fmt.Errorf("Unable to list the LXD containers: %s", err) } images, err := c.ListImages() if err != nil { return fmt.Errorf("Unable to list the LXD images: %s", err) } if len(containers) > 0 || len(images) > 0 { return fmt.Errorf("You have existing containers or images. lxd init requires an empty LXD.") } if *argAuto { if *argStorageBackend == "" { *argStorageBackend = "dir" } // Do a bunch of sanity checks if !shared.StringInSlice(*argStorageBackend, backendsSupported) { return fmt.Errorf("The requested backend '%s' isn't supported by lxd init.", *argStorageBackend) } if !shared.StringInSlice(*argStorageBackend, backendsAvailable) { return fmt.Errorf("The requested backend '%s' isn't available on your system (missing tools).", *argStorageBackend) } if *argStorageBackend == "dir" { if *argStorageCreateLoop != -1 || *argStorageCreateDevice != "" || *argStoragePool != "" { return fmt.Errorf("None of --storage-pool, --storage-create-device or --storage-create-loop may be used with the 'dir' backend.") } } if *argStorageBackend == "zfs" { if *argStorageCreateLoop != -1 && *argStorageCreateDevice != "" { return fmt.Errorf("Only one of --storage-create-device or --storage-create-loop can be specified with the 'zfs' backend.") } if *argStoragePool == "" { return fmt.Errorf("--storage-pool must be specified with the 'zfs' backend.") } } if *argNetworkAddress == "" { if *argNetworkPort != -1 { return fmt.Errorf("--network-port cannot be used without --network-address.") } if *argTrustPassword != "" { return fmt.Errorf("--trust-password cannot be used without --network-address.") } } // Set the local variables if *argStorageCreateDevice != "" { storageMode = "device" } else if *argStorageCreateLoop != -1 { storageMode = "loop" } else { storageMode = "existing" } storageBackend = *argStorageBackend storageLoopSize = *argStorageCreateLoop storageDevice = *argStorageCreateDevice storagePool = *argStoragePool networkAddress = *argNetworkAddress networkPort = *argNetworkPort trustPassword = *argTrustPassword } else { if *argStorageBackend != "" || *argStorageCreateDevice != "" || *argStorageCreateLoop != -1 || *argStoragePool != "" || *argNetworkAddress != "" || *argNetworkPort != -1 || *argTrustPassword != "" { return fmt.Errorf("Init configuration is only valid with --auto") } defaultStorage := "dir" if shared.StringInSlice("zfs", backendsAvailable) { defaultStorage = "zfs" } storageBackend = askChoice(fmt.Sprintf("Name of the storage backend to use (dir or zfs) [default=%s]: ", defaultStorage), backendsSupported, defaultStorage) if !shared.StringInSlice(storageBackend, backendsSupported) { return fmt.Errorf("The requested backend '%s' isn't supported by lxd init.", storageBackend) } if !shared.StringInSlice(storageBackend, backendsAvailable) { return fmt.Errorf("The requested backend '%s' isn't available on your system (missing tools).", storageBackend) } if storageBackend == "zfs" { if askBool("Create a new ZFS pool (yes/no) [default=yes]? ", "yes") { storagePool = askString("Name of the new ZFS pool [default=lxd]: ", "lxd", nil) if askBool("Would you like to use an existing block device (yes/no) [default=no]? ", "no") { deviceExists := func(path string) error { if !shared.IsBlockdevPath(path) { return fmt.Errorf("'%s' is not a block device", path) } return nil } storageDevice = askString("Path to the existing block device: ", "", deviceExists) storageMode = "device" } else { st := syscall.Statfs_t{} err := syscall.Statfs(shared.VarPath(), &st) if err != nil { return fmt.Errorf("couldn't statfs %s: %s", shared.VarPath(), err) } /* choose 15 GB < x < 100GB, where x is 20% of the disk size */ def := uint64(st.Frsize) * st.Blocks / (1024 * 1024 * 1024) / 5 if def > 100 { def = 100 } if def < 15 { def = 15 } q := fmt.Sprintf("Size in GB of the new loop device (1GB minimum) [default=%d]: ", def) storageLoopSize = askInt(q, 1, -1, fmt.Sprintf("%d", def)) storageMode = "loop" } } else { storagePool = askString("Name of the existing ZFS pool or dataset: ", "", nil) storageMode = "existing" } } if runningInUserns { fmt.Printf(` We detected that you are running inside an unprivileged container. This means that unless you manually configured your host otherwise, you will not have enough uid and gid to allocate to your containers. LXD can re-use your container's own allocation to avoid the problem. Doing so makes your nested containers slightly less safe as they could in theory attack their parent container and gain more privileges than they otherwise would. `) if askBool("Would you like to have your containers share their parent's allocation (yes/no) [default=yes]? ", "yes") { defaultPrivileged = 1 } else { defaultPrivileged = 0 } } if askBool("Would you like LXD to be available over the network (yes/no) [default=no]? ", "no") { isIPAddress := func(s string) error { if s != "all" && net.ParseIP(s) == nil { return fmt.Errorf("'%s' is not an IP address", s) } return nil } networkAddress = askString("Address to bind LXD to (not including port) [default=all]: ", "all", isIPAddress) if networkAddress == "all" { networkAddress = "::" } if net.ParseIP(networkAddress).To4() == nil { networkAddress = fmt.Sprintf("[%s]", networkAddress) } networkPort = askInt("Port to bind LXD to [default=8443]: ", 1, 65535, "8443") trustPassword = askPassword("Trust password for new clients: ") } if !askBool("Would you like stale cached images to be updated automatically (yes/no) [default=yes]? ", "yes") { imagesAutoUpdate = false } if askBool("Would you like to create a new network bridge (yes/no) [default=yes]? ", "yes") { bridgeName = askString("What should the new bridge be called [default=lxdbr0]? ", "lxdbr0", networkValidName) bridgeIPv4 = askString("What IPv4 subnet should be used (CIDR notation, “auto” or “none”) [default=auto]? ", "auto", func(value string) error { if shared.StringInSlice(value, []string{"auto", "none"}) { return nil } return networkValidAddressCIDRV4(value) }) if !shared.StringInSlice(bridgeIPv4, []string{"auto", "none"}) { bridgeIPv4Nat = askBool("Would you like LXD to NAT IPv4 traffic on your bridge? [default=yes]? ", "yes") } bridgeIPv6 = askString("What IPv6 subnet should be used (CIDR notation, “auto” or “none”) [default=auto]? ", "auto", func(value string) error { if shared.StringInSlice(value, []string{"auto", "none"}) { return nil } return networkValidAddressCIDRV6(value) }) if !shared.StringInSlice(bridgeIPv6, []string{"auto", "none"}) { bridgeIPv6Nat = askBool("Would you like LXD to NAT IPv6 traffic on your bridge? [default=yes]? ", "yes") } } } if !shared.StringInSlice(storageBackend, []string{"dir", "zfs"}) { return fmt.Errorf("Invalid storage backend: %s", storageBackend) } // Unset all storage keys, core.https_address and core.trust_password for _, key := range []string{"storage.zfs_pool_name", "core.https_address", "core.trust_password"} { _, err = c.SetServerConfig(key, "") if err != nil { return err } } // Destroy any existing loop device for _, file := range []string{"zfs.img"} { os.Remove(shared.VarPath(file)) } if storageBackend == "zfs" { if storageMode == "loop" { storageDevice = shared.VarPath("zfs.img") f, err := os.Create(storageDevice) if err != nil { return fmt.Errorf("Failed to open %s: %s", storageDevice, err) } err = f.Chmod(0600) if err != nil { return fmt.Errorf("Failed to chmod %s: %s", storageDevice, err) } err = f.Truncate(int64(storageLoopSize * 1024 * 1024 * 1024)) if err != nil { return fmt.Errorf("Failed to create sparse file %s: %s", storageDevice, err) } err = f.Close() if err != nil { return fmt.Errorf("Failed to close %s: %s", storageDevice, err) } } if shared.StringInSlice(storageMode, []string{"loop", "device"}) { output, err := exec.Command( "zpool", "create", storagePool, storageDevice, "-f", "-m", "none", "-O", "compression=on").CombinedOutput() if err != nil { return fmt.Errorf("Failed to create the ZFS pool: %s", output) } } // Configure LXD to use the pool _, err = c.SetServerConfig("storage.zfs_pool_name", storagePool) if err != nil { return err } } if defaultPrivileged == 0 { err = c.SetProfileConfigItem("default", "security.privileged", "") if err != nil { return err } } else if defaultPrivileged == 1 { err = c.SetProfileConfigItem("default", "security.privileged", "true") if err != nil { } } if imagesAutoUpdate { ss, err := c.ServerStatus() if err != nil { return err } if val, ok := ss.Config["images.auto_update_interval"]; ok && val == "0" { _, err = c.SetServerConfig("images.auto_update_interval", "") if err != nil { return err } } } else { _, err = c.SetServerConfig("images.auto_update_interval", "0") if err != nil { return err } } if networkAddress != "" { _, err = c.SetServerConfig("core.https_address", fmt.Sprintf("%s:%d", networkAddress, networkPort)) if err != nil { return err } if trustPassword != "" { _, err = c.SetServerConfig("core.trust_password", trustPassword) if err != nil { return err } } } if bridgeName != "" { bridgeConfig := map[string]string{} bridgeConfig["ipv4.address"] = bridgeIPv4 bridgeConfig["ipv6.address"] = bridgeIPv6 if bridgeIPv4Nat { bridgeConfig["ipv4.nat"] = "true" } if bridgeIPv6Nat { bridgeConfig["ipv6.nat"] = "true" } err = c.NetworkCreate(bridgeName, bridgeConfig) if err != nil { return err } props := []string{"nictype=bridged", fmt.Sprintf("parent=%s", bridgeName)} _, err = c.ProfileDeviceAdd("default", "eth0", "nic", props) if err != nil { return err } } fmt.Printf("LXD has been successfully configured.\n") return nil }
func deviceGetParentBlocks(path string) ([]string, error) { var devices []string var device []string // Expand the mount path absPath, err := filepath.Abs(path) if err != nil { return nil, err } expPath, err := filepath.EvalSymlinks(absPath) if err != nil { expPath = absPath } // Find the source mount of the path file, err := os.Open("/proc/self/mountinfo") if err != nil { return nil, err } defer file.Close() scanner := bufio.NewScanner(file) match := "" for scanner.Scan() { line := scanner.Text() rows := strings.Fields(line) if len(rows[4]) <= len(match) { continue } if expPath != rows[4] && !strings.HasPrefix(expPath, rows[4]) { continue } match = rows[4] // Go backward to avoid problems with optional fields device = []string{rows[2], rows[len(rows)-2]} } if device == nil { return nil, fmt.Errorf("Couldn't find a match /proc/self/mountinfo entry") } // Handle the most simple case if !strings.HasPrefix(device[0], "0:") { return []string{device[0]}, nil } // Deal with per-filesystem oddities. We don't care about failures here // because any non-special filesystem => directory backend. fs, _ := filesystemDetect(expPath) if fs == "zfs" && shared.PathExists("/dev/zfs") { // Accessible zfs filesystems poolName := strings.Split(device[1], "/")[0] output, err := exec.Command("zpool", "status", poolName).CombinedOutput() if err != nil { return nil, fmt.Errorf("Failed to query zfs filesystem information for %s: %s", device[1], output) } for _, line := range strings.Split(string(output), "\n") { fields := strings.Fields(line) if len(fields) < 5 { continue } if fields[1] != "ONLINE" { continue } var path string if shared.PathExists(fields[0]) { if shared.IsBlockdevPath(fields[0]) { path = fields[0] } else { subDevices, err := deviceGetParentBlocks(fields[0]) if err != nil { return nil, err } for _, dev := range subDevices { devices = append(devices, dev) } } } else if shared.PathExists(fmt.Sprintf("/dev/%s", fields[0])) { path = fmt.Sprintf("/dev/%s", fields[0]) } else if shared.PathExists(fmt.Sprintf("/dev/disk/by-id/%s", fields[0])) { path = fmt.Sprintf("/dev/disk/by-id/%s", fields[0]) } else { return nil, fmt.Errorf("Unsupported zfs backing device: %s", fields[0]) } if path != "" { _, major, minor, err := deviceGetAttributes(fields[len(fields)-1]) if err != nil { return nil, err } devices = append(devices, fmt.Sprintf("%d:%d", major, minor)) } } } else if fs == "btrfs" && shared.PathExists(device[1]) { // Accessible btrfs filesystems output, err := exec.Command("btrfs", "filesystem", "show", device[1]).CombinedOutput() if err != nil { return nil, fmt.Errorf("Failed to query btrfs filesystem information for %s: %s", device[1], output) } for _, line := range strings.Split(string(output), "\n") { fields := strings.Fields(line) if len(fields) == 0 || fields[0] != "devid" { continue } _, major, minor, err := deviceGetAttributes(fields[len(fields)-1]) if err != nil { return nil, err } devices = append(devices, fmt.Sprintf("%d:%d", major, minor)) } } else if shared.PathExists(device[1]) { // Anything else with a valid path _, major, minor, err := deviceGetAttributes(device[1]) if err != nil { return nil, err } devices = append(devices, fmt.Sprintf("%d:%d", major, minor)) } else { return nil, fmt.Errorf("Invalid block device: %s", device[1]) } return devices, nil }
func deviceToLxc(cntPath string, d shared.Device) ([][]string, error) { switch d["type"] { case "unix-char": return unixDevCgroup(d) case "unix-block": return unixDevCgroup(d) case "nic": // A few checks if d["nictype"] == "" { return nil, fmt.Errorf("Missing nic type") } if !shared.StringInSlice(d["nictype"], []string{"bridged", "physical", "p2p", "macvlan"}) { return nil, fmt.Errorf("Bad nic type: %s", d["nictype"]) } if shared.StringInSlice(d["nictype"], []string{"bridged", "physical", "macvlan"}) && d["parent"] == "" { return nil, fmt.Errorf("Missing parent for %s type nic.", d["nictype"]) } // Generate the LXC config var line []string var lines = [][]string{} if shared.StringInSlice(d["nictype"], []string{"bridged", "p2p"}) { line = []string{"lxc.network.type", "veth"} lines = append(lines, line) } else if d["nictype"] == "physical" { line = []string{"lxc.network.type", "phys"} lines = append(lines, line) } else if d["nictype"] == "macvlan" { line = []string{"lxc.network.type", "macvlan"} lines = append(lines, line) line = []string{"lxc.network.macvlan.mode", "bridge"} lines = append(lines, line) } if d["hwaddr"] != "" { line = []string{"lxc.network.hwaddr", d["hwaddr"]} lines = append(lines, line) } if d["mtu"] != "" { line = []string{"lxc.network.mtu", d["mtu"]} lines = append(lines, line) } if shared.StringInSlice(d["nictype"], []string{"bridged", "physical", "macvlan"}) { line = []string{"lxc.network.link", d["parent"]} lines = append(lines, line) } if d["name"] != "" { line = []string{"lxc.network.name", d["name"]} lines = append(lines, line) } return lines, nil case "disk": var p string configLines := [][]string{} if d["path"] == "/" || d["path"] == "" { p = "" } else if d["path"][0:1] == "/" { p = d["path"][1:] } else { p = d["path"] } source := d["source"] options := []string{} if shared.IsBlockdevPath(d["source"]) { l, err := mountTmpBlockdev(cntPath, d) if err != nil { return nil, fmt.Errorf("Error adding blockdev: %s", err) } configLines = append(configLines, l) return configLines, nil } else if shared.IsDir(source) { options = append(options, "bind") options = append(options, "create=dir") } else /* file bind mount */ { /* Todo - can we distinguish between file bind mount and * a qcow2 (or other fs container) file? */ options = append(options, "bind") options = append(options, "create=file") } if d["readonly"] == "1" || d["readonly"] == "true" { options = append(options, "ro") } if d["optional"] == "1" || d["optional"] == "true" { options = append(options, "optional") } opts := strings.Join(options, ",") if opts == "" { opts = "defaults" } l := []string{"lxc.mount.entry", fmt.Sprintf("%s %s %s %s 0 0", source, p, "none", opts)} configLines = append(configLines, l) return configLines, nil case "none": return nil, nil default: return nil, fmt.Errorf("Bad device type") } }