Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
645 lines
20 KiB
Go
645 lines
20 KiB
Go
/*
|
|
SystemD Backend - Container runtime using systemd-nspawn, machinectl, and nsenter.
|
|
|
|
This backend implements the ContainerBackend interface using:
|
|
- systemd-nspawn for container creation and execution
|
|
- machinectl for container lifecycle and inspection
|
|
- nsenter for exec into running containers
|
|
- journalctl for container logs
|
|
- systemctl for service management
|
|
*/
|
|
package systemd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/armoredgate/volt/pkg/backend"
|
|
)
|
|
|
|
func init() {
|
|
backend.Register("systemd", func() backend.ContainerBackend { return New() })
|
|
}
|
|
|
|
const (
|
|
defaultContainerBaseDir = "/var/lib/volt/containers"
|
|
defaultImageBaseDir = "/var/lib/volt/images"
|
|
unitPrefix = "volt-container@"
|
|
unitDir = "/etc/systemd/system"
|
|
)
|
|
|
|
// Backend implements backend.ContainerBackend using systemd-nspawn.
|
|
type Backend struct {
|
|
containerBaseDir string
|
|
imageBaseDir string
|
|
}
|
|
|
|
// New creates a new SystemD backend with default paths.
|
|
func New() *Backend {
|
|
return &Backend{
|
|
containerBaseDir: defaultContainerBaseDir,
|
|
imageBaseDir: defaultImageBaseDir,
|
|
}
|
|
}
|
|
|
|
// Name returns "systemd".
|
|
func (b *Backend) Name() string { return "systemd" }
|
|
|
|
// Available returns true if systemd-nspawn is installed.
|
|
func (b *Backend) Available() bool {
|
|
_, err := exec.LookPath("systemd-nspawn")
|
|
return err == nil
|
|
}
|
|
|
|
// Init initializes the backend, optionally overriding the data directory.
|
|
func (b *Backend) Init(dataDir string) error {
|
|
if dataDir != "" {
|
|
b.containerBaseDir = filepath.Join(dataDir, "containers")
|
|
b.imageBaseDir = filepath.Join(dataDir, "images")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ── Capability flags ─────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) SupportsVMs() bool { return true }
|
|
func (b *Backend) SupportsServices() bool { return true }
|
|
func (b *Backend) SupportsNetworking() bool { return true }
|
|
func (b *Backend) SupportsTuning() bool { return true }
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
// unitName returns the systemd unit name for a container.
|
|
func unitName(name string) string {
|
|
return fmt.Sprintf("volt-container@%s.service", name)
|
|
}
|
|
|
|
// unitFilePath returns the full path to a container's service unit file.
|
|
func unitFilePath(name string) string {
|
|
return filepath.Join(unitDir, unitName(name))
|
|
}
|
|
|
|
// containerDir returns the rootfs dir for a container.
|
|
func (b *Backend) containerDir(name string) string {
|
|
return filepath.Join(b.containerBaseDir, name)
|
|
}
|
|
|
|
// runCommand executes a command and returns combined output.
|
|
func runCommand(name string, args ...string) (string, error) {
|
|
cmd := exec.Command(name, args...)
|
|
out, err := cmd.CombinedOutput()
|
|
return strings.TrimSpace(string(out)), err
|
|
}
|
|
|
|
// runCommandSilent executes a command and returns stdout only.
|
|
func runCommandSilent(name string, args ...string) (string, error) {
|
|
cmd := exec.Command(name, args...)
|
|
out, err := cmd.Output()
|
|
return strings.TrimSpace(string(out)), err
|
|
}
|
|
|
|
// runCommandInteractive executes a command with stdin/stdout/stderr attached.
|
|
func runCommandInteractive(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// fileExists returns true if the file exists.
|
|
func fileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
// dirExists returns true if the directory exists.
|
|
func dirExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return info.IsDir()
|
|
}
|
|
|
|
// resolveImagePath resolves an --image value to a directory path.
|
|
func (b *Backend) resolveImagePath(img string) (string, error) {
|
|
if dirExists(img) {
|
|
return img, nil
|
|
}
|
|
normalized := strings.ReplaceAll(img, ":", "_")
|
|
candidates := []string{
|
|
filepath.Join(b.imageBaseDir, img),
|
|
filepath.Join(b.imageBaseDir, normalized),
|
|
}
|
|
for _, p := range candidates {
|
|
if dirExists(p) {
|
|
return p, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("image %q not found (checked %s)", img, strings.Join(candidates, ", "))
|
|
}
|
|
|
|
// writeUnitFile writes the systemd-nspawn service unit for a container.
|
|
// Uses --as-pid2: nspawn provides a stub init as PID 1 that handles signal
|
|
// forwarding and zombie reaping. No init system required inside the container.
|
|
func writeUnitFile(name string) error {
|
|
unit := `[Unit]
|
|
Description=Volt Container: %i
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --as-pid2 --machine=%i --directory=/var/lib/volt/containers/%i --network-bridge=voltbr0 -- sleep infinity
|
|
KillMode=mixed
|
|
Restart=on-failure
|
|
|
|
[Install]
|
|
WantedBy=machines.target
|
|
`
|
|
return os.WriteFile(unitFilePath(name), []byte(unit), 0644)
|
|
}
|
|
|
|
// daemonReload runs systemctl daemon-reload.
|
|
func daemonReload() error {
|
|
_, err := runCommand("systemctl", "daemon-reload")
|
|
return err
|
|
}
|
|
|
|
// isContainerRunning checks if a container is currently running.
|
|
func isContainerRunning(name string) bool {
|
|
out, err := runCommandSilent("machinectl", "show", name, "--property=State")
|
|
if err == nil && strings.Contains(out, "running") {
|
|
return true
|
|
}
|
|
out, err = runCommandSilent("systemctl", "is-active", unitName(name))
|
|
if err == nil && strings.TrimSpace(out) == "active" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getContainerLeaderPID returns the leader PID of a running container.
|
|
func getContainerLeaderPID(name string) (string, error) {
|
|
out, err := runCommandSilent("machinectl", "show", name, "--property=Leader")
|
|
if err == nil {
|
|
parts := strings.SplitN(out, "=", 2)
|
|
if len(parts) == 2 {
|
|
pid := strings.TrimSpace(parts[1])
|
|
if pid != "" && pid != "0" {
|
|
return pid, nil
|
|
}
|
|
}
|
|
}
|
|
out, err = runCommandSilent("systemctl", "show", unitName(name), "--property=MainPID")
|
|
if err == nil {
|
|
parts := strings.SplitN(out, "=", 2)
|
|
if len(parts) == 2 {
|
|
pid := strings.TrimSpace(parts[1])
|
|
if pid != "" && pid != "0" {
|
|
return pid, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no running PID found for container %q", name)
|
|
}
|
|
|
|
// resolveContainerCommand resolves a bare command name to an absolute path
|
|
// inside the container's rootfs.
|
|
func (b *Backend) resolveContainerCommand(name, cmd string) string {
|
|
if strings.HasPrefix(cmd, "/") {
|
|
return cmd
|
|
}
|
|
rootfs := b.containerDir(name)
|
|
searchDirs := []string{
|
|
"usr/bin", "bin", "usr/sbin", "sbin",
|
|
"usr/local/bin", "usr/local/sbin",
|
|
}
|
|
for _, dir := range searchDirs {
|
|
candidate := filepath.Join(rootfs, dir, cmd)
|
|
if fileExists(candidate) {
|
|
return "/" + dir + "/" + cmd
|
|
}
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
// ── Create ───────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Create(opts backend.CreateOptions) error {
|
|
destDir := b.containerDir(opts.Name)
|
|
|
|
if dirExists(destDir) {
|
|
return fmt.Errorf("container %q already exists at %s", opts.Name, destDir)
|
|
}
|
|
|
|
fmt.Printf("Creating container: %s\n", opts.Name)
|
|
|
|
if opts.Image != "" {
|
|
srcDir, err := b.resolveImagePath(opts.Image)
|
|
if err != nil {
|
|
return fmt.Errorf("image resolution failed: %w", err)
|
|
}
|
|
fmt.Printf(" Image: %s → %s\n", opts.Image, srcDir)
|
|
|
|
if err := os.MkdirAll(b.containerBaseDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create container base dir: %w", err)
|
|
}
|
|
|
|
fmt.Printf(" Copying rootfs...\n")
|
|
out, err := runCommand("cp", "-a", srcDir, destDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy image rootfs: %s", out)
|
|
}
|
|
} else {
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create container dir: %w", err)
|
|
}
|
|
}
|
|
|
|
if opts.Memory != "" {
|
|
fmt.Printf(" Memory: %s\n", opts.Memory)
|
|
}
|
|
if opts.Network != "" {
|
|
fmt.Printf(" Network: %s\n", opts.Network)
|
|
}
|
|
|
|
if err := writeUnitFile(opts.Name); err != nil {
|
|
fmt.Printf(" Warning: could not write unit file: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" Unit: %s\n", unitFilePath(opts.Name))
|
|
}
|
|
|
|
nspawnConfigDir := "/etc/systemd/nspawn"
|
|
os.MkdirAll(nspawnConfigDir, 0755)
|
|
nspawnConfig := "[Exec]\nBoot=no\n\n[Network]\nBridge=voltbr0\n"
|
|
if opts.Memory != "" {
|
|
nspawnConfig += fmt.Sprintf("\n[ResourceControl]\nMemoryMax=%s\n", opts.Memory)
|
|
}
|
|
configPath := filepath.Join(nspawnConfigDir, opts.Name+".nspawn")
|
|
if err := os.WriteFile(configPath, []byte(nspawnConfig), 0644); err != nil {
|
|
fmt.Printf(" Warning: could not write nspawn config: %v\n", err)
|
|
}
|
|
|
|
if err := daemonReload(); err != nil {
|
|
fmt.Printf(" Warning: daemon-reload failed: %v\n", err)
|
|
}
|
|
|
|
fmt.Printf("\nContainer %s created.\n", opts.Name)
|
|
|
|
if opts.Start {
|
|
fmt.Printf("Starting container %s...\n", opts.Name)
|
|
out, err := runCommand("systemctl", "start", unitName(opts.Name))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start container: %s", out)
|
|
}
|
|
fmt.Printf("Container %s started.\n", opts.Name)
|
|
} else {
|
|
fmt.Printf("Start with: volt container start %s\n", opts.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ── Start ────────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Start(name string) error {
|
|
unitFile := unitFilePath(name)
|
|
if !fileExists(unitFile) {
|
|
return fmt.Errorf("container %q does not exist (no unit file at %s)", name, unitFile)
|
|
}
|
|
fmt.Printf("Starting container: %s\n", name)
|
|
out, err := runCommand("systemctl", "start", unitName(name))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start container %s: %s", name, out)
|
|
}
|
|
fmt.Printf("Container %s started.\n", name)
|
|
return nil
|
|
}
|
|
|
|
// ── Stop ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Stop(name string) error {
|
|
fmt.Printf("Stopping container: %s\n", name)
|
|
out, err := runCommand("systemctl", "stop", unitName(name))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stop container %s: %s", name, out)
|
|
}
|
|
fmt.Printf("Container %s stopped.\n", name)
|
|
return nil
|
|
}
|
|
|
|
// ── Delete ───────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Delete(name string, force bool) error {
|
|
rootfs := b.containerDir(name)
|
|
|
|
unitActive, _ := runCommandSilent("systemctl", "is-active", unitName(name))
|
|
if strings.TrimSpace(unitActive) == "active" || strings.TrimSpace(unitActive) == "activating" {
|
|
if !force {
|
|
return fmt.Errorf("container %q is running — stop it first or use --force", name)
|
|
}
|
|
fmt.Printf("Stopping container %s...\n", name)
|
|
runCommand("systemctl", "stop", unitName(name))
|
|
}
|
|
|
|
fmt.Printf("Deleting container: %s\n", name)
|
|
|
|
unitPath := unitFilePath(name)
|
|
if fileExists(unitPath) {
|
|
runCommand("systemctl", "disable", unitName(name))
|
|
if err := os.Remove(unitPath); err != nil {
|
|
fmt.Printf(" Warning: could not remove unit file: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" Removed unit: %s\n", unitPath)
|
|
}
|
|
}
|
|
|
|
nspawnConfig := filepath.Join("/etc/systemd/nspawn", name+".nspawn")
|
|
if fileExists(nspawnConfig) {
|
|
os.Remove(nspawnConfig)
|
|
}
|
|
|
|
if dirExists(rootfs) {
|
|
if err := os.RemoveAll(rootfs); err != nil {
|
|
return fmt.Errorf("failed to remove rootfs at %s: %w", rootfs, err)
|
|
}
|
|
fmt.Printf(" Removed rootfs: %s\n", rootfs)
|
|
}
|
|
|
|
daemonReload()
|
|
|
|
fmt.Printf("Container %s deleted.\n", name)
|
|
return nil
|
|
}
|
|
|
|
// ── Exec ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Exec(name string, opts backend.ExecOptions) error {
|
|
cmdArgs := opts.Command
|
|
if len(cmdArgs) == 0 {
|
|
cmdArgs = []string{"/bin/sh"}
|
|
}
|
|
|
|
// Resolve bare command names to absolute paths inside the container
|
|
cmdArgs[0] = b.resolveContainerCommand(name, cmdArgs[0])
|
|
|
|
pid, err := getContainerLeaderPID(name)
|
|
if err != nil {
|
|
return fmt.Errorf("container %q is not running: %w", name, err)
|
|
}
|
|
|
|
nsenterArgs := []string{"-t", pid, "-m", "-u", "-i", "-n", "-p", "--"}
|
|
nsenterArgs = append(nsenterArgs, cmdArgs...)
|
|
return runCommandInteractive("nsenter", nsenterArgs...)
|
|
}
|
|
|
|
// ── Logs ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Logs(name string, opts backend.LogOptions) (string, error) {
|
|
jArgs := []string{"-u", unitName(name), "--no-pager"}
|
|
if opts.Follow {
|
|
jArgs = append(jArgs, "-f")
|
|
}
|
|
if opts.Tail > 0 {
|
|
jArgs = append(jArgs, "-n", fmt.Sprintf("%d", opts.Tail))
|
|
} else {
|
|
jArgs = append(jArgs, "-n", "100")
|
|
}
|
|
|
|
// For follow mode, run interactively so output streams to terminal
|
|
if opts.Follow {
|
|
return "", runCommandInteractive("journalctl", jArgs...)
|
|
}
|
|
|
|
out, err := runCommand("journalctl", jArgs...)
|
|
return out, err
|
|
}
|
|
|
|
// ── CopyToContainer ──────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) CopyToContainer(name string, src string, dst string) error {
|
|
if !fileExists(src) && !dirExists(src) {
|
|
return fmt.Errorf("source not found: %s", src)
|
|
}
|
|
dstPath := filepath.Join(b.containerDir(name), dst)
|
|
out, err := runCommand("cp", "-a", src, dstPath)
|
|
if err != nil {
|
|
return fmt.Errorf("copy failed: %s", out)
|
|
}
|
|
fmt.Printf("Copied %s → %s:%s\n", src, name, dst)
|
|
return nil
|
|
}
|
|
|
|
// ── CopyFromContainer ────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) CopyFromContainer(name string, src string, dst string) error {
|
|
srcPath := filepath.Join(b.containerDir(name), src)
|
|
if !fileExists(srcPath) && !dirExists(srcPath) {
|
|
return fmt.Errorf("not found in container %s: %s", name, src)
|
|
}
|
|
out, err := runCommand("cp", "-a", srcPath, dst)
|
|
if err != nil {
|
|
return fmt.Errorf("copy failed: %s", out)
|
|
}
|
|
fmt.Printf("Copied %s:%s → %s\n", name, src, dst)
|
|
return nil
|
|
}
|
|
|
|
// ── List ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) List() ([]backend.ContainerInfo, error) {
|
|
var containers []backend.ContainerInfo
|
|
seen := make(map[string]bool)
|
|
|
|
// Get running containers from machinectl
|
|
out, err := runCommandSilent("machinectl", "list", "--no-pager", "--no-legend")
|
|
if err == nil && strings.TrimSpace(out) != "" {
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 {
|
|
continue
|
|
}
|
|
name := fields[0]
|
|
seen[name] = true
|
|
|
|
info := backend.ContainerInfo{
|
|
Name: name,
|
|
Status: "running",
|
|
RootFS: b.containerDir(name),
|
|
}
|
|
|
|
// Get IP from machinectl show
|
|
showOut, showErr := runCommandSilent("machinectl", "show", name,
|
|
"--property=Addresses", "--property=RootDirectory")
|
|
if showErr == nil {
|
|
for _, sl := range strings.Split(showOut, "\n") {
|
|
if strings.HasPrefix(sl, "Addresses=") {
|
|
addr := strings.TrimPrefix(sl, "Addresses=")
|
|
if addr != "" {
|
|
info.IPAddress = addr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read OS from rootfs
|
|
rootfs := b.containerDir(name)
|
|
if osRel, osErr := os.ReadFile(filepath.Join(rootfs, "etc", "os-release")); osErr == nil {
|
|
for _, ol := range strings.Split(string(osRel), "\n") {
|
|
if strings.HasPrefix(ol, "PRETTY_NAME=") {
|
|
info.OS = strings.Trim(strings.TrimPrefix(ol, "PRETTY_NAME="), "\"")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
containers = append(containers, info)
|
|
}
|
|
}
|
|
|
|
// Scan filesystem for stopped containers
|
|
if entries, err := os.ReadDir(b.containerBaseDir); err == nil {
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
|
|
info := backend.ContainerInfo{
|
|
Name: name,
|
|
Status: "stopped",
|
|
RootFS: filepath.Join(b.containerBaseDir, name),
|
|
}
|
|
|
|
if osRel, err := os.ReadFile(filepath.Join(b.containerBaseDir, name, "etc", "os-release")); err == nil {
|
|
for _, ol := range strings.Split(string(osRel), "\n") {
|
|
if strings.HasPrefix(ol, "PRETTY_NAME=") {
|
|
info.OS = strings.Trim(strings.TrimPrefix(ol, "PRETTY_NAME="), "\"")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
containers = append(containers, info)
|
|
}
|
|
}
|
|
|
|
return containers, nil
|
|
}
|
|
|
|
// ── Inspect ──────────────────────────────────────────────────────────────────
|
|
|
|
func (b *Backend) Inspect(name string) (*backend.ContainerInfo, error) {
|
|
rootfs := b.containerDir(name)
|
|
|
|
info := &backend.ContainerInfo{
|
|
Name: name,
|
|
RootFS: rootfs,
|
|
Status: "stopped",
|
|
}
|
|
|
|
if !dirExists(rootfs) {
|
|
info.Status = "not found"
|
|
}
|
|
|
|
// Check if running
|
|
unitActive, _ := runCommandSilent("systemctl", "is-active", unitName(name))
|
|
activeState := strings.TrimSpace(unitActive)
|
|
if activeState == "active" {
|
|
info.Status = "running"
|
|
} else if activeState != "" {
|
|
info.Status = activeState
|
|
}
|
|
|
|
// Get machinectl info if running
|
|
if isContainerRunning(name) {
|
|
info.Status = "running"
|
|
showOut, err := runCommandSilent("machinectl", "show", name)
|
|
if err == nil {
|
|
for _, line := range strings.Split(showOut, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "Addresses=") {
|
|
info.IPAddress = strings.TrimPrefix(line, "Addresses=")
|
|
}
|
|
if strings.HasPrefix(line, "Leader=") {
|
|
pidStr := strings.TrimPrefix(line, "Leader=")
|
|
fmt.Sscanf(pidStr, "%d", &info.PID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// OS info from rootfs
|
|
if osRel, err := os.ReadFile(filepath.Join(rootfs, "etc", "os-release")); err == nil {
|
|
for _, line := range strings.Split(string(osRel), "\n") {
|
|
if strings.HasPrefix(line, "PRETTY_NAME=") {
|
|
info.OS = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// ── Extra methods used by CLI commands (not in the interface) ────────────────
|
|
|
|
// IsContainerRunning checks if a container is currently running.
|
|
// Exported for use by CLI commands that need direct state checks.
|
|
func (b *Backend) IsContainerRunning(name string) bool {
|
|
return isContainerRunning(name)
|
|
}
|
|
|
|
// GetContainerLeaderPID returns the leader PID of a running container.
|
|
// Exported for use by CLI commands (shell, attach).
|
|
func (b *Backend) GetContainerLeaderPID(name string) (string, error) {
|
|
return getContainerLeaderPID(name)
|
|
}
|
|
|
|
// ContainerDir returns the rootfs dir for a container.
|
|
// Exported for use by CLI commands that need rootfs access.
|
|
func (b *Backend) ContainerDir(name string) string {
|
|
return b.containerDir(name)
|
|
}
|
|
|
|
// UnitName returns the systemd unit name for a container.
|
|
// Exported for use by CLI commands.
|
|
func UnitName(name string) string {
|
|
return unitName(name)
|
|
}
|
|
|
|
// UnitFilePath returns the full path to a container's service unit file.
|
|
// Exported for use by CLI commands.
|
|
func UnitFilePath(name string) string {
|
|
return unitFilePath(name)
|
|
}
|
|
|
|
// WriteUnitFile writes the systemd-nspawn service unit for a container.
|
|
// Exported for use by CLI commands (rename).
|
|
func WriteUnitFile(name string) error {
|
|
return writeUnitFile(name)
|
|
}
|
|
|
|
// DaemonReload runs systemctl daemon-reload.
|
|
// Exported for use by CLI commands.
|
|
func DaemonReload() error {
|
|
return daemonReload()
|
|
}
|
|
|
|
// ResolveContainerCommand resolves a bare command to an absolute path in the container.
|
|
// Exported for use by CLI commands (shell).
|
|
func (b *Backend) ResolveContainerCommand(name, cmd string) string {
|
|
return b.resolveContainerCommand(name, cmd)
|
|
}
|