158 lines
4.1 KiB
Go
158 lines
4.1 KiB
Go
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 := 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
|
|
}
|