package lxc import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" ) // CmdManager uses lxc-* commands. type CmdManager struct { ConfigDir string Template string // unused placeholder for future rendering } func NewCmdManager(cfgDir string) *CmdManager { return &CmdManager{ConfigDir: cfgDir} } func (m *CmdManager) List() ([]Container, error) { cmd := exec.Command("lxc-ls", "--active") out, err := cmd.Output() if err != nil { return nil, err } names := splitLines(string(out)) var res []Container for _, n := range names { if n == "" { continue } res = append(res, Container{ID: n, Name: n, Status: "running", Unpriv: true}) } return res, nil } func (m *CmdManager) Create(spec Spec) (Container, error) { if spec.Name == "" { spec.Name = spec.ID } if err := os.MkdirAll(m.ConfigDir, 0o755); err != nil { return Container{}, err } // For simplicity, use download template; real code should render rootfs according to spec. args := []string{"-n", spec.Name, "-t", "download", "--", "-d", "debian", "-r", "bookworm", "-a", "amd64"} if err := exec.Command("lxc-create", args...).Run(); err != nil { return Container{}, err } cfgPath := filepath.Join(m.ConfigDir, fmt.Sprintf("%s.conf", spec.Name)) cfgContent, err := renderConfig(spec) if err != nil { return Container{}, err } _ = os.WriteFile(cfgPath, []byte(cfgContent), 0o644) return Container{ID: spec.Name, Name: spec.Name, Status: "stopped", Unpriv: spec.Unprivileged}, nil } func (m *CmdManager) Start(id string) error { return exec.Command("lxc-start", "-n", id).Run() } func (m *CmdManager) Stop(id string) error { return exec.Command("lxc-stop", "-n", id).Run() } func (m *CmdManager) Delete(id string) error { return exec.Command("lxc-destroy", "-n", id).Run() } func splitLines(s string) []string { var out []string start := 0 for i := 0; i < len(s); i++ { if s[i] == '\n' || s[i] == '\r' { if start < i { out = append(out, s[start:i]) } start = i + 1 } } if start < len(s) { out = append(out, s[start:]) } return out } func renderConfig(spec Spec) (string, error) { buf := &bytes.Buffer{} fmt.Fprintf(buf, "lxc.include = /usr/share/lxc/config/common.conf\n") fmt.Fprintf(buf, "lxc.arch = linux64\n") if spec.Unprivileged { fmt.Fprintf(buf, "lxc.apparmor.profile = generated\n") fmt.Fprintf(buf, "lxc.apparmor.allow_nesting = 1\n") uidStart, count, ok := hostIDMap() if !ok { return "", fmt.Errorf("no subuid/subgid ranges found for root; configure /etc/subuid and /etc/subgid") } fmt.Fprintf(buf, "lxc.idmap = u 0 %d %d\n", uidStart, count) fmt.Fprintf(buf, "lxc.idmap = g 0 %d %d\n", uidStart, count) } rootfs := spec.RootfsPath if rootfs == "" { rootfs = fmt.Sprintf("/var/lib/lxc/%s/rootfs", spec.Name) } fmt.Fprintf(buf, "lxc.rootfs.path = dir:%s\n", rootfs) for idx, nic := range spec.NICs { fmt.Fprintf(buf, "lxc.net.%d.type = veth\n", idx) fmt.Fprintf(buf, "lxc.net.%d.link = %s\n", idx, nic.Bridge) if nic.VLAN > 0 { fmt.Fprintf(buf, "lxc.net.%d.vlan.id = %d\n", idx, nic.VLAN) } if nic.HWAddr != "" { fmt.Fprintf(buf, "lxc.net.%d.hwaddr = %s\n", idx, nic.HWAddr) } if nic.MTU > 0 { fmt.Fprintf(buf, "lxc.net.%d.mtu = %d\n", idx, nic.MTU) } if nic.Name != "" { fmt.Fprintf(buf, "lxc.net.%d.name = %s\n", idx, nic.Name) } } return buf.String(), nil } // hostIDMap finds the first subuid/subgid range for root, defaulting to 100000/65536. func hostIDMap() (start int, count int, ok bool) { contentUID, errUID := os.ReadFile("/etc/subuid") contentGID, errGID := os.ReadFile("/etc/subgid") if errUID != nil || errGID != nil { return 0, 0, false } s1, c1 := parseSubID(contentUID) s2, c2 := parseSubID(contentGID) if s1 > 0 && c1 > 0 && s2 > 0 && c2 > 0 { return s1, c1, true } return 0, 0, false } func parseSubID(b []byte) (int, int) { lines := strings.Split(string(b), "\n") for _, line := range lines { fields := strings.Split(line, ":") if len(fields) < 3 { continue } if fields[0] != "root" { continue } s, err1 := strconv.Atoi(fields[1]) c, err2 := strconv.Atoi(fields[2]) if err1 == nil && err2 == nil { return s, c } } return 0, 0 }