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") } if spec.RootfsPool == "" { return fmt.Errorf("rootfs_pool 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))] }