Volt CLI: source-available under AGPSL v5.0
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
This commit is contained in:
438
pkg/kernel/manager.go
Normal file
438
pkg/kernel/manager.go
Normal file
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
Kernel Manager - Download, verify, and manage kernels for Volt hybrid runtime.
|
||||
|
||||
Provides kernel lifecycle operations:
|
||||
- Download kernels to /var/lib/volt/kernels/
|
||||
- Verify SHA-256 checksums
|
||||
- List available (local) kernels
|
||||
- Default kernel selection (host kernel fallback)
|
||||
- Kernel config validation (namespaces, cgroups, Landlock)
|
||||
|
||||
Copyright (c) Armored Gates LLC. All rights reserved.
|
||||
*/
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultKernelDir is where kernels are stored on disk.
|
||||
DefaultKernelDir = "/var/lib/volt/kernels"
|
||||
|
||||
// HostKernelPath is the default host kernel image location.
|
||||
HostKernelPath = "/boot/vmlinuz"
|
||||
|
||||
// configGzPath is the compressed kernel config inside /proc.
|
||||
configGzPath = "/proc/config.gz"
|
||||
)
|
||||
|
||||
// KernelInfo describes a locally available kernel.
|
||||
type KernelInfo struct {
|
||||
Version string // e.g. "6.1.0-42-amd64"
|
||||
Path string // absolute path to vmlinuz
|
||||
Size int64 // bytes
|
||||
SHA256 string // hex-encoded checksum
|
||||
Source string // "host", "downloaded", "custom"
|
||||
AddedAt time.Time // when the kernel was registered
|
||||
IsDefault bool // whether this is the active default
|
||||
}
|
||||
|
||||
// RequiredFeature is a kernel config option that must be present.
|
||||
type RequiredFeature struct {
|
||||
Config string // e.g. "CONFIG_NAMESPACES"
|
||||
Description string // human-readable explanation
|
||||
}
|
||||
|
||||
// RequiredFeatures lists kernel config options needed for Volt hybrid mode.
|
||||
var RequiredFeatures = []RequiredFeature{
|
||||
{Config: "CONFIG_NAMESPACES", Description: "Namespace support (PID, NET, MNT, UTS, IPC)"},
|
||||
{Config: "CONFIG_PID_NS", Description: "PID namespace isolation"},
|
||||
{Config: "CONFIG_NET_NS", Description: "Network namespace isolation"},
|
||||
{Config: "CONFIG_USER_NS", Description: "User namespace isolation"},
|
||||
{Config: "CONFIG_UTS_NS", Description: "UTS namespace isolation"},
|
||||
{Config: "CONFIG_IPC_NS", Description: "IPC namespace isolation"},
|
||||
{Config: "CONFIG_CGROUPS", Description: "Control groups support"},
|
||||
{Config: "CONFIG_CGROUP_V2", Description: "Cgroups v2 unified hierarchy"},
|
||||
{Config: "CONFIG_SECURITY_LANDLOCK", Description: "Landlock LSM filesystem sandboxing"},
|
||||
{Config: "CONFIG_SECCOMP", Description: "Seccomp syscall filtering"},
|
||||
{Config: "CONFIG_SECCOMP_FILTER", Description: "Seccomp BPF filter programs"},
|
||||
}
|
||||
|
||||
// Manager handles kernel downloads, verification, and selection.
|
||||
type Manager struct {
|
||||
kernelDir string
|
||||
}
|
||||
|
||||
// NewManager creates a new kernel manager rooted at the given directory.
|
||||
// If kernelDir is empty, DefaultKernelDir is used.
|
||||
func NewManager(kernelDir string) *Manager {
|
||||
if kernelDir == "" {
|
||||
kernelDir = DefaultKernelDir
|
||||
}
|
||||
return &Manager{kernelDir: kernelDir}
|
||||
}
|
||||
|
||||
// Init ensures the kernel directory exists.
|
||||
func (m *Manager) Init() error {
|
||||
return os.MkdirAll(m.kernelDir, 0755)
|
||||
}
|
||||
|
||||
// KernelDir returns the base directory for kernel storage.
|
||||
func (m *Manager) KernelDir() string {
|
||||
return m.kernelDir
|
||||
}
|
||||
|
||||
// ── Download & Verify ────────────────────────────────────────────────────────
|
||||
|
||||
// Download fetches a kernel image from url into the kernel directory under the
|
||||
// given version name. If expectedSHA256 is non-empty the download is verified
|
||||
// against it; a mismatch causes the file to be removed and an error returned.
|
||||
func (m *Manager) Download(version, url, expectedSHA256 string) (*KernelInfo, error) {
|
||||
if err := m.Init(); err != nil {
|
||||
return nil, fmt.Errorf("kernel dir init: %w", err)
|
||||
}
|
||||
|
||||
destDir := filepath.Join(m.kernelDir, version)
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create version dir: %w", err)
|
||||
}
|
||||
|
||||
destPath := filepath.Join(destDir, "vmlinuz")
|
||||
|
||||
// Download to temp file first, then rename.
|
||||
tmpPath := destPath + ".tmp"
|
||||
out, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
out.Close()
|
||||
os.Remove(tmpPath) // clean up on any failure path
|
||||
}()
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Minute}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("download returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
writer := io.MultiWriter(out, hasher)
|
||||
|
||||
if _, err := io.Copy(writer, resp.Body); err != nil {
|
||||
return nil, fmt.Errorf("download interrupted: %w", err)
|
||||
}
|
||||
|
||||
if err := out.Close(); err != nil {
|
||||
return nil, fmt.Errorf("close temp file: %w", err)
|
||||
}
|
||||
|
||||
checksum := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
if expectedSHA256 != "" && !strings.EqualFold(checksum, expectedSHA256) {
|
||||
os.Remove(tmpPath)
|
||||
return nil, fmt.Errorf("checksum mismatch: got %s, expected %s", checksum, expectedSHA256)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, destPath); err != nil {
|
||||
return nil, fmt.Errorf("rename to final path: %w", err)
|
||||
}
|
||||
|
||||
// Write checksum sidecar.
|
||||
checksumPath := filepath.Join(destDir, "sha256")
|
||||
os.WriteFile(checksumPath, []byte(checksum+"\n"), 0644)
|
||||
|
||||
fi, _ := os.Stat(destPath)
|
||||
return &KernelInfo{
|
||||
Version: version,
|
||||
Path: destPath,
|
||||
Size: fi.Size(),
|
||||
SHA256: checksum,
|
||||
Source: "downloaded",
|
||||
AddedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyChecksum checks that the kernel at path matches the expected SHA-256
|
||||
// hex digest. Returns nil on match.
|
||||
func VerifyChecksum(path, expectedSHA256 string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open kernel: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return fmt.Errorf("read kernel: %w", err)
|
||||
}
|
||||
|
||||
got := hex.EncodeToString(h.Sum(nil))
|
||||
if !strings.EqualFold(got, expectedSHA256) {
|
||||
return fmt.Errorf("checksum mismatch: got %s, expected %s", got, expectedSHA256)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checksum computes and returns the SHA-256 hex digest of the file at path.
|
||||
func Checksum(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", fmt.Errorf("read: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// ── List ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// List returns all locally available kernels sorted by version name.
|
||||
func (m *Manager) List() ([]KernelInfo, error) {
|
||||
entries, err := os.ReadDir(m.kernelDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read kernel dir: %w", err)
|
||||
}
|
||||
|
||||
var kernels []KernelInfo
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
version := entry.Name()
|
||||
vmlinuz := filepath.Join(m.kernelDir, version, "vmlinuz")
|
||||
fi, err := os.Stat(vmlinuz)
|
||||
if err != nil {
|
||||
continue // not a valid kernel directory
|
||||
}
|
||||
|
||||
ki := KernelInfo{
|
||||
Version: version,
|
||||
Path: vmlinuz,
|
||||
Size: fi.Size(),
|
||||
Source: "downloaded",
|
||||
}
|
||||
|
||||
// Read checksum sidecar if present.
|
||||
if data, err := os.ReadFile(filepath.Join(m.kernelDir, version, "sha256")); err == nil {
|
||||
ki.SHA256 = strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
kernels = append(kernels, ki)
|
||||
}
|
||||
|
||||
sort.Slice(kernels, func(i, j int) bool {
|
||||
return kernels[i].Version < kernels[j].Version
|
||||
})
|
||||
|
||||
return kernels, nil
|
||||
}
|
||||
|
||||
// ── Default Kernel Selection ─────────────────────────────────────────────────
|
||||
|
||||
// DefaultKernel returns the best kernel to use:
|
||||
// 1. The host kernel at /boot/vmlinuz-$(uname -r).
|
||||
// 2. Generic /boot/vmlinuz fallback.
|
||||
// 3. The latest locally downloaded kernel.
|
||||
//
|
||||
// Returns the absolute path to the kernel image.
|
||||
func (m *Manager) DefaultKernel() (string, error) {
|
||||
// Prefer the host kernel matching the running version.
|
||||
uname := currentKernelVersion()
|
||||
hostPath := "/boot/vmlinuz-" + uname
|
||||
if fileExists(hostPath) {
|
||||
return hostPath, nil
|
||||
}
|
||||
|
||||
// Generic fallback.
|
||||
if fileExists(HostKernelPath) {
|
||||
return HostKernelPath, nil
|
||||
}
|
||||
|
||||
// Check locally downloaded kernels — pick the latest.
|
||||
kernels, err := m.List()
|
||||
if err == nil && len(kernels) > 0 {
|
||||
return kernels[len(kernels)-1].Path, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no kernel found (checked %s, %s, %s)", hostPath, HostKernelPath, m.kernelDir)
|
||||
}
|
||||
|
||||
// ResolveKernel resolves a kernel reference to an absolute path.
|
||||
// If kernelRef is an absolute path and exists, it is returned directly.
|
||||
// Otherwise, it is treated as a version name under kernelDir.
|
||||
// If empty, DefaultKernel() is used.
|
||||
func (m *Manager) ResolveKernel(kernelRef string) (string, error) {
|
||||
if kernelRef == "" {
|
||||
return m.DefaultKernel()
|
||||
}
|
||||
|
||||
// Absolute path — use directly.
|
||||
if filepath.IsAbs(kernelRef) {
|
||||
if !fileExists(kernelRef) {
|
||||
return "", fmt.Errorf("kernel not found: %s", kernelRef)
|
||||
}
|
||||
return kernelRef, nil
|
||||
}
|
||||
|
||||
// Treat as version name.
|
||||
path := filepath.Join(m.kernelDir, kernelRef, "vmlinuz")
|
||||
if fileExists(path) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("kernel version %q not found in %s", kernelRef, m.kernelDir)
|
||||
}
|
||||
|
||||
// ── Kernel Config Validation ─────────────────────────────────────────────────
|
||||
|
||||
// ValidationResult holds the outcome of a kernel config check.
|
||||
type ValidationResult struct {
|
||||
Feature RequiredFeature
|
||||
Present bool
|
||||
Value string // "y", "m", or empty
|
||||
}
|
||||
|
||||
// ValidateHostKernel checks the running host kernel's config for required
|
||||
// features. It reads from /boot/config-$(uname -r) or /proc/config.gz.
|
||||
func ValidateHostKernel() ([]ValidationResult, error) {
|
||||
uname := currentKernelVersion()
|
||||
configPath := "/boot/config-" + uname
|
||||
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
// Try /proc/config.gz via zcat
|
||||
configData, err = readProcConfigGz()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read kernel config (tried %s and %s): %w",
|
||||
configPath, configGzPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return validateConfig(string(configData)), nil
|
||||
}
|
||||
|
||||
// ValidateConfigFile checks a kernel config file at the given path for
|
||||
// required features.
|
||||
func ValidateConfigFile(path string) ([]ValidationResult, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config file: %w", err)
|
||||
}
|
||||
return validateConfig(string(data)), nil
|
||||
}
|
||||
|
||||
// validateConfig parses a kernel .config text and checks for required features.
|
||||
func validateConfig(configText string) []ValidationResult {
|
||||
configMap := make(map[string]string)
|
||||
for _, line := range strings.Split(configText, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Check for "# CONFIG_FOO is not set" pattern.
|
||||
if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, " is not set") {
|
||||
key := strings.TrimPrefix(line, "# ")
|
||||
key = strings.TrimSuffix(key, " is not set")
|
||||
configMap[key] = "n"
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
configMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
var results []ValidationResult
|
||||
for _, feat := range RequiredFeatures {
|
||||
val := configMap[feat.Config]
|
||||
r := ValidationResult{Feature: feat}
|
||||
if val == "y" || val == "m" {
|
||||
r.Present = true
|
||||
r.Value = val
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// AllFeaturesPresent returns true if every validation result is present.
|
||||
func AllFeaturesPresent(results []ValidationResult) bool {
|
||||
for _, r := range results {
|
||||
if !r.Present {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MissingFeatures returns only the features that are not present.
|
||||
func MissingFeatures(results []ValidationResult) []ValidationResult {
|
||||
var missing []ValidationResult
|
||||
for _, r := range results {
|
||||
if !r.Present {
|
||||
missing = append(missing, r)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// currentKernelVersion returns the running kernel version string (uname -r).
|
||||
func currentKernelVersion() string {
|
||||
data, err := os.ReadFile("/proc/sys/kernel/osrelease")
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
// Fallback: shell out to uname.
|
||||
out, err := exec.Command("uname", "-r").Output()
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// readProcConfigGz reads kernel config from /proc/config.gz using zcat.
|
||||
func readProcConfigGz() ([]byte, error) {
|
||||
if !fileExists(configGzPath) {
|
||||
return nil, fmt.Errorf("%s not found (try: modprobe configs)", configGzPath)
|
||||
}
|
||||
return exec.Command("zcat", configGzPath).Output()
|
||||
}
|
||||
|
||||
// fileExists returns true if the path exists and is not a directory.
|
||||
func fileExists(path string) bool {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fi.IsDir()
|
||||
}
|
||||
Reference in New Issue
Block a user