Files
jagacloud/pkg/state/store.go
2025-11-23 11:29:12 +07:00

203 lines
4.1 KiB
Go

package state
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
"jagacloud/node-agent/pkg/compute/libvirt"
"jagacloud/node-agent/pkg/containers/lxc"
)
// Store persists VM/CT specs to disk and provides simple locks per ID.
type Store struct {
vmDir string
ctDir string
mu sync.Mutex
locks map[string]*sync.Mutex
}
func NewStore(vmDir, ctDir string) *Store {
return &Store{vmDir: vmDir, ctDir: ctDir, locks: make(map[string]*sync.Mutex)}
}
func (s *Store) ensureDirs() error {
for _, d := range []string{s.vmDir, s.ctDir} {
if err := os.MkdirAll(d, 0o755); err != nil {
return err
}
}
return nil
}
// ---- Locks ----
func (s *Store) lock(id string) func() {
s.mu.Lock()
m, ok := s.locks[id]
if !ok {
m = &sync.Mutex{}
s.locks[id] = m
}
s.mu.Unlock()
m.Lock()
return m.Unlock
}
// Lock exposes a per-ID lock; caller must defer the returned unlock function.
func (s *Store) Lock(id string) func() {
return s.lock(id)
}
// ---- VM ----
func (s *Store) SaveVM(spec libvirt.VMSpec) error {
if err := s.ensureDirs(); err != nil {
return err
}
if spec.ID == "" && spec.Name != "" {
spec.ID = spec.Name
}
if spec.ID == "" {
return fmt.Errorf("vm id is required")
}
data, err := yaml.Marshal(spec)
if err != nil {
return err
}
path := filepath.Join(s.vmDir, fmt.Sprintf("%s.yaml", spec.ID))
return os.WriteFile(path, data, 0o644)
}
func (s *Store) LoadVM(id string) (libvirt.VMSpec, error) {
var spec libvirt.VMSpec
path := filepath.Join(s.vmDir, fmt.Sprintf("%s.yaml", id))
data, err := os.ReadFile(path)
if err != nil {
return spec, err
}
if err := yaml.Unmarshal(data, &spec); err != nil {
return spec, err
}
if spec.ID == "" {
spec.ID = id
}
return spec, nil
}
func (s *Store) ListVMs() ([]libvirt.VMSpec, error) {
specs := []libvirt.VMSpec{}
err := filepath.WalkDir(s.vmDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if filepath.Ext(path) != ".yaml" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var spec libvirt.VMSpec
if err := yaml.Unmarshal(data, &spec); err != nil {
return err
}
if spec.ID == "" {
spec.ID = trimExt(filepath.Base(path))
}
specs = append(specs, spec)
return nil
})
if err != nil && !os.IsNotExist(err) {
return nil, err
}
return specs, nil
}
func (s *Store) DeleteVM(id string) error {
path := filepath.Join(s.vmDir, fmt.Sprintf("%s.yaml", id))
return os.Remove(path)
}
// ---- CT ----
func (s *Store) SaveCT(spec lxc.Spec) error {
if err := s.ensureDirs(); err != nil {
return err
}
if spec.ID == "" && spec.Name != "" {
spec.ID = spec.Name
}
if spec.ID == "" {
return fmt.Errorf("ct id is required")
}
data, err := yaml.Marshal(spec)
if err != nil {
return err
}
path := filepath.Join(s.ctDir, fmt.Sprintf("%s.yaml", spec.ID))
return os.WriteFile(path, data, 0o644)
}
func (s *Store) LoadCT(id string) (lxc.Spec, error) {
var spec lxc.Spec
path := filepath.Join(s.ctDir, fmt.Sprintf("%s.yaml", id))
data, err := os.ReadFile(path)
if err != nil {
return spec, err
}
if err := yaml.Unmarshal(data, &spec); err != nil {
return spec, err
}
if spec.ID == "" {
spec.ID = id
}
return spec, nil
}
func (s *Store) ListCTs() ([]lxc.Spec, error) {
specs := []lxc.Spec{}
err := filepath.WalkDir(s.ctDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if filepath.Ext(path) != ".yaml" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var spec lxc.Spec
if err := yaml.Unmarshal(data, &spec); err != nil {
return err
}
if spec.ID == "" {
spec.ID = trimExt(filepath.Base(path))
}
specs = append(specs, spec)
return nil
})
if err != nil && !os.IsNotExist(err) {
return nil, err
}
return specs, nil
}
func (s *Store) DeleteCT(id string) error {
path := filepath.Join(s.ctDir, fmt.Sprintf("%s.yaml", id))
return os.Remove(path)
}
func trimExt(name string) string {
return name[:len(name)-len(filepath.Ext(name))]
}