Skip to content

Commit 7655ab9

Browse files
committed
gc for vm
1 parent e76a200 commit 7655ab9

7 files changed

Lines changed: 290 additions & 25 deletions

File tree

config/cloudhypervisor.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package config
2+
3+
import (
4+
"path/filepath"
5+
6+
"github.com/projecteru2/cocoon/utils"
7+
)
8+
9+
// EnsureCHDirs creates all static directories required by the Cloud Hypervisor backend.
10+
// Per-VM runtime and log directories are created on demand via EnsureCHVMDirs.
11+
func (c *Config) EnsureCHDirs() error {
12+
return utils.EnsureDirs(
13+
c.chDBDir(),
14+
c.chLogDir(),
15+
)
16+
}
17+
18+
// EnsureCHVMDirs creates per-VM runtime and log directories.
19+
// Called when a VM is created or started.
20+
func (c *Config) EnsureCHVMDirs(vmID string) error {
21+
return utils.EnsureDirs(
22+
c.CHVMRunDir(vmID),
23+
c.CHVMLogDir(vmID),
24+
)
25+
}
26+
27+
// Derived path helpers. All CH persistent data lives under {RootDir}/cloudhypervisor/.
28+
29+
func (c *Config) chDir() string { return filepath.Join(c.RootDir, "cloudhypervisor") }
30+
func (c *Config) chDBDir() string { return filepath.Join(c.chDir(), "db") }
31+
32+
// CHIndexFile and CHIndexLock are the VM index store paths.
33+
func (c *Config) CHIndexFile() string { return filepath.Join(c.chDBDir(), "vms.json") }
34+
func (c *Config) CHIndexLock() string { return filepath.Join(c.chDBDir(), "vms.lock") }
35+
36+
// Runtime paths (per VM, under RunDir — ephemeral).
37+
38+
func (c *Config) CHVMRunDir(vmID string) string { return filepath.Join(c.RunDir, "cloudhypervisor", vmID) }
39+
func (c *Config) CHVMSocketPath(vmID string) string { return filepath.Join(c.CHVMRunDir(vmID), "api.sock") }
40+
func (c *Config) CHVMPIDFile(vmID string) string { return filepath.Join(c.CHVMRunDir(vmID), "ch.pid") }
41+
42+
// Log paths (per VM, under {LogDir}/cloudhypervisor/).
43+
// Namespaced under "cloudhypervisor" so future backends (e.g. qemu) scan
44+
// their own subdirectory and never conflict during GC.
45+
46+
func (c *Config) chLogDir() string { return filepath.Join(c.LogDir, "cloudhypervisor") }
47+
func (c *Config) CHVMLogDir(vmID string) string { return filepath.Join(c.chLogDir(), vmID) }
48+
func (c *Config) CHVMSerialLog(vmID string) string { return filepath.Join(c.CHVMLogDir(vmID), "serial.log") }
49+
func (c *Config) CHVMProcessLog(vmID string) string { return filepath.Join(c.CHVMLogDir(vmID), "ch.log") }
50+
51+
// FirmwarePath returns the path to the UEFI firmware blob (CLOUDHV.fd).
52+
func (c *Config) FirmwarePath() string {
53+
return filepath.Join(c.RootDir, "firmware", "CLOUDHV.fd")
54+
}

config/config.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,24 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7-
"path/filepath"
7+
88
"runtime"
99

1010
coretypes "github.com/projecteru2/core/types"
1111
)
1212

1313
// Config holds global Cocoon configuration.
1414
type Config struct {
15-
// RootDir is the base directory for persistent data.
15+
// RootDir is the base directory for persistent data (images, firmware, VM DB).
16+
// Env: COCOON_ROOT_DIR. Default: /var/lib/cocoon.
1617
RootDir string `json:"root_dir"`
18+
// RunDir is the base directory for runtime state (PID files, Unix sockets).
19+
// Contents are ephemeral and may not survive reboots.
20+
// Env: COCOON_RUN_DIR. Default: /var/run/cocoon.
21+
RunDir string `json:"run_dir"`
22+
// LogDir is the base directory for VM and process logs.
23+
// Env: COCOON_LOG_DIR. Default: /var/log/cocoon.
24+
LogDir string `json:"log_dir"`
1725
// PoolSize is the goroutine pool size for concurrent operations.
1826
// Defaults to runtime.NumCPU() if zero.
1927
PoolSize int `json:"pool_size"`
@@ -25,6 +33,8 @@ type Config struct {
2533
func DefaultConfig() *Config {
2634
return &Config{
2735
RootDir: "/var/lib/cocoon",
36+
RunDir: "/var/run/cocoon",
37+
LogDir: "/var/log/cocoon",
2838
PoolSize: runtime.NumCPU(),
2939
Log: coretypes.ServerLogConfig{
3040
Level: "info",
@@ -60,7 +70,3 @@ func LoadConfig(path string) (*Config, error) {
6070
return conf, nil
6171
}
6272

63-
// FirmwarePath returns the path to the UEFI firmware file (CLOUDHV.fd).
64-
func (c *Config) FirmwarePath() string {
65-
return filepath.Join(c.RootDir, "firmware", "CLOUDHV.fd")
66-
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cloudhypervisor
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/projecteru2/cocoon/config"
8+
"github.com/projecteru2/cocoon/hypervisor"
9+
"github.com/projecteru2/cocoon/lock"
10+
"github.com/projecteru2/cocoon/lock/flock"
11+
storejson "github.com/projecteru2/cocoon/storage/json"
12+
"github.com/projecteru2/cocoon/storage"
13+
"github.com/projecteru2/cocoon/types"
14+
)
15+
16+
const typ = "cloud-hypervisor"
17+
18+
// CloudHypervisor implements hypervisor.Hypervisor using the Cloud Hypervisor VMM.
19+
type CloudHypervisor struct {
20+
conf *config.Config
21+
store storage.Store[hypervisor.VMIndex]
22+
locker lock.Locker
23+
}
24+
25+
// New creates a CloudHypervisor backend.
26+
func New(conf *config.Config) (*CloudHypervisor, error) {
27+
if err := conf.EnsureCHDirs(); err != nil {
28+
return nil, fmt.Errorf("ensure dirs: %w", err)
29+
}
30+
locker := flock.New(conf.CHIndexLock())
31+
store := storejson.New[hypervisor.VMIndex](conf.CHIndexFile(), locker)
32+
return &CloudHypervisor{conf: conf, store: store, locker: locker}, nil
33+
}
34+
35+
func (ch *CloudHypervisor) Type() string { return typ }
36+
37+
// Create, Start, Stop, Inspect, List, Delete — to be implemented.
38+
39+
func (ch *CloudHypervisor) Create(_ context.Context, _ *types.VMConfig, _ []*types.StorageConfig, _ *types.BootConfig) (*types.VMInfo, error) {
40+
panic("not implemented")
41+
}
42+
43+
func (ch *CloudHypervisor) Start(_ context.Context, _ []string) ([]string, error) {
44+
panic("not implemented")
45+
}
46+
47+
func (ch *CloudHypervisor) Stop(_ context.Context, _ []string) ([]string, error) {
48+
panic("not implemented")
49+
}
50+
51+
func (ch *CloudHypervisor) Inspect(_ context.Context, _ string) (*types.VMInfo, error) {
52+
panic("not implemented")
53+
}
54+
55+
func (ch *CloudHypervisor) List(_ context.Context) ([]*types.VMInfo, error) {
56+
panic("not implemented")
57+
}
58+
59+
func (ch *CloudHypervisor) Delete(_ context.Context, _ []string) ([]string, error) {
60+
panic("not implemented")
61+
}
62+

hypervisor/cloudhypervisor/gc.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cloudhypervisor
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/projecteru2/cocoon/gc"
10+
"github.com/projecteru2/cocoon/hypervisor"
11+
"github.com/projecteru2/cocoon/utils"
12+
)
13+
14+
// compile-time interface check.
15+
var _ hypervisor.Hypervisor = (*CloudHypervisor)(nil)
16+
17+
// chSnapshot is the typed GC snapshot for the Cloud Hypervisor backend.
18+
type chSnapshot struct {
19+
vmIDs map[string]struct{} // VM IDs present in the DB index
20+
runDirs []string // VM IDs that have a runtime dir on disk
21+
logDirs []string // VM IDs that have a log dir on disk
22+
}
23+
24+
// GCModule returns a typed gc.Module[chSnapshot] for the Cloud Hypervisor backend.
25+
//
26+
// ReadDB (under lock): reads the VM index and scans the runtime/log base dirs
27+
// on disk, returning a snapshot of what exists vs what is recorded.
28+
//
29+
// Resolve: returns VM IDs whose runtime or log dirs are orphaned (present on
30+
// disk but no longer in the DB).
31+
//
32+
// Collect (under lock): removes the orphaned dirs for each VM ID.
33+
func (ch *CloudHypervisor) GCModule() gc.Module[chSnapshot] {
34+
return gc.Module[chSnapshot]{
35+
Name: typ,
36+
Locker: ch.locker,
37+
ReadDB: func(ctx context.Context) (chSnapshot, error) {
38+
var snap chSnapshot
39+
if err := ch.store.Read(func(idx *hypervisor.VMIndex) error {
40+
snap.vmIDs = make(map[string]struct{}, len(idx.VMs))
41+
for id := range idx.VMs {
42+
snap.vmIDs[id] = struct{}{}
43+
}
44+
return nil
45+
}); err != nil {
46+
return snap, err
47+
}
48+
snap.runDirs = utils.ScanSubdirs(filepath.Join(ch.conf.RunDir, "cloudhypervisor"))
49+
snap.logDirs = utils.ScanSubdirs(filepath.Join(ch.conf.LogDir, "cloudhypervisor"))
50+
return snap, nil
51+
},
52+
Resolve: func(snap chSnapshot, _ map[string]any) []string {
53+
unreferenced := utils.FilterUnreferenced(snap.runDirs, snap.vmIDs)
54+
seen := make(map[string]struct{}, len(unreferenced))
55+
for _, id := range unreferenced {
56+
seen[id] = struct{}{}
57+
}
58+
for _, id := range utils.FilterUnreferenced(snap.logDirs, snap.vmIDs) {
59+
if _, already := seen[id]; !already {
60+
unreferenced = append(unreferenced, id)
61+
}
62+
}
63+
return unreferenced
64+
},
65+
Collect: func(ctx context.Context, ids []string) error {
66+
var errs []error
67+
for _, vmID := range ids {
68+
if err := os.RemoveAll(ch.conf.CHVMRunDir(vmID)); err != nil && !os.IsNotExist(err) {
69+
errs = append(errs, err)
70+
}
71+
if err := os.RemoveAll(ch.conf.CHVMLogDir(vmID)); err != nil && !os.IsNotExist(err) {
72+
errs = append(errs, err)
73+
}
74+
}
75+
return errors.Join(errs...)
76+
},
77+
}
78+
}
79+
80+
// RegisterGC registers the Cloud Hypervisor GC module with the given Orchestrator.
81+
func (ch *CloudHypervisor) RegisterGC(orch *gc.Orchestrator) {
82+
gc.Register(orch, ch.GCModule())
83+
}

hypervisor/db.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package hypervisor
2+
3+
import "github.com/projecteru2/cocoon/types"
4+
5+
// VMRecord is the persisted record for a single VM.
6+
// It extends types.VMInfo with the disk and boot configuration needed to
7+
// restart a VM without re-resolving the image.
8+
//
9+
// PID and SocketPath on types.VMInfo are NOT stored here:
10+
// - SocketPath is deterministic: derived from config at query time.
11+
// - PID changes on every start; Inspect reads the live value from the PID
12+
// file instead, avoiding stale PIDs after a crash or reboot.
13+
type VMRecord struct {
14+
types.VMInfo
15+
16+
// StorageConfigs holds the ordered disk attachments at creation time
17+
// (EROFS layers first, then the COW disk). Persisted so that a stopped
18+
// VM can be restarted with the same disk layout.
19+
StorageConfigs []*types.StorageConfig `json:"storage_configs"`
20+
21+
// BootConfig holds the kernel and initrd paths for direct-boot VMs.
22+
// Nil for UEFI-boot VMs (cloud images).
23+
BootConfig *types.BootConfig `json:"boot_config,omitempty"`
24+
}
25+
26+
// VMIndex is the top-level DB structure shared by all hypervisor backends.
27+
// Each backend stores its VMs under a separate index file
28+
// (e.g. {RootDir}/cloudhypervisor/db/vms.json).
29+
type VMIndex struct {
30+
VMs map[string]*VMRecord `json:"vms"`
31+
}
32+
33+
// Init implements storage.Initer — initialises the nil map after deserialization.
34+
func (idx *VMIndex) Init() {
35+
if idx.VMs == nil {
36+
idx.VMs = make(map[string]*VMRecord)
37+
}
38+
}

images/index.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -149,28 +149,11 @@ func ListImages[E Entry](images map[string]*E, typ string, sizer func(*E) int64)
149149
}
150150

151151
// ScanBlobHexes returns the digest hexes of all files with the given suffix in dir.
152-
// Used by GC ReadDB implementations to build the on-disk candidate set.
153-
func ScanBlobHexes(dir, suffix string) []string {
154-
entries, _ := os.ReadDir(dir)
155-
var hexes []string
156-
for _, e := range entries {
157-
if strings.HasSuffix(e.Name(), suffix) {
158-
hexes = append(hexes, strings.TrimSuffix(e.Name(), suffix))
159-
}
160-
}
161-
return hexes
162-
}
152+
func ScanBlobHexes(dir, suffix string) []string { return utils.ScanFileStems(dir, suffix) }
163153

164154
// FilterUnreferenced returns elements of candidates not present in refs.
165-
// Used by GC Resolve implementations to compute the deletion set.
166155
func FilterUnreferenced(candidates []string, refs map[string]struct{}) []string {
167-
var out []string
168-
for _, s := range candidates {
169-
if _, ok := refs[s]; !ok {
170-
out = append(out, s)
171-
}
172-
}
173-
return out
156+
return utils.FilterUnreferenced(candidates, refs)
174157
}
175158

176159
// GCStaleTemp removes temp entries older than StaleTempAge.

utils/file.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"strings"
89
"time"
910

1011
"github.com/projecteru2/core/log"
@@ -29,6 +30,44 @@ func ValidFile(path string) bool {
2930
return err == nil && info.Mode().IsRegular() && info.Size() > 0
3031
}
3132

33+
// ScanFileStems returns the name-without-suffix of every file in dir whose
34+
// name ends with suffix. Used by GC to enumerate on-disk blobs.
35+
func ScanFileStems(dir, suffix string) []string {
36+
entries, _ := os.ReadDir(dir)
37+
var stems []string
38+
for _, e := range entries {
39+
if !e.IsDir() && strings.HasSuffix(e.Name(), suffix) {
40+
stems = append(stems, strings.TrimSuffix(e.Name(), suffix))
41+
}
42+
}
43+
return stems
44+
}
45+
46+
// ScanSubdirs returns the names of all immediate subdirectories of dir.
47+
// Used by GC to enumerate per-VM runtime and log directories.
48+
func ScanSubdirs(dir string) []string {
49+
entries, _ := os.ReadDir(dir)
50+
var names []string
51+
for _, e := range entries {
52+
if e.IsDir() {
53+
names = append(names, e.Name())
54+
}
55+
}
56+
return names
57+
}
58+
59+
// FilterUnreferenced returns the elements of candidates not present in refs.
60+
// Used by GC Resolve implementations to compute the deletion set.
61+
func FilterUnreferenced(candidates []string, refs map[string]struct{}) []string {
62+
var out []string
63+
for _, s := range candidates {
64+
if _, ok := refs[s]; !ok {
65+
out = append(out, s)
66+
}
67+
}
68+
return out
69+
}
70+
3271
// RemoveMatching scans dir and removes entries where match returns true.
3372
// Returns a slice of errors for entries that could not be removed.
3473
func RemoveMatching(ctx context.Context, dir string, match func(os.DirEntry) bool) []error {

0 commit comments

Comments
 (0)