Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f62a47003d | |||
| 5abaed9047 | |||
| c702d649e0 | |||
| e9f9d474dc | |||
| e2941cf2c0 | |||
| a0cc698a39 | |||
| 70008e0ead | |||
| ec2b30c1e7 |
+38
-1
@@ -1,11 +1,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "git.jpi.io/amery/jpictl/pkg/cluster"
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"darvaza.org/core"
|
||||||
|
|
||||||
|
"git.jpi.io/amery/jpictl/pkg/cluster"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultConfigFile is read if -f/--config-file isn't specified.
|
||||||
|
// If it doesn't exist, m/ will be scanned
|
||||||
|
DefaultConfigFile = "cloud.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
// Config describes the repository
|
// Config describes the repository
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Base string
|
Base string
|
||||||
Domain string
|
Domain string
|
||||||
|
|
||||||
|
ConfigFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg = &Config{
|
var cfg = &Config{
|
||||||
@@ -14,9 +28,32 @@ var cfg = &Config{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadZones loads all zones and machines in the config directory
|
// LoadZones loads all zones and machines in the config directory
|
||||||
|
// or file
|
||||||
func (cfg *Config) LoadZones(resolve bool) (*cluster.Cluster, error) {
|
func (cfg *Config) LoadZones(resolve bool) (*cluster.Cluster, error) {
|
||||||
|
// try config file first
|
||||||
|
zones, err := cluster.NewFromConfig(cfg.ConfigFile,
|
||||||
|
cluster.ResolvePublicAddresses(resolve),
|
||||||
|
cluster.WithLogger(log),
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
// file was good
|
||||||
|
return zones, nil
|
||||||
|
case !os.IsNotExist(err) || cfg.ConfigFile != DefaultConfigFile:
|
||||||
|
// file was bad
|
||||||
|
return nil, core.Wrapf(err, "NewFromConfig(%q)", cfg.ConfigFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default file doesn't exist. scan instead.
|
||||||
return cluster.NewFromDirectory(cfg.Base, cfg.Domain,
|
return cluster.NewFromDirectory(cfg.Base, cfg.Domain,
|
||||||
cluster.ResolvePublicAddresses(resolve),
|
cluster.ResolvePublicAddresses(resolve),
|
||||||
cluster.WithLogger(log),
|
cluster.WithLogger(log),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().
|
||||||
|
StringVarP(&cfg.ConfigFile, "config-file", "f",
|
||||||
|
DefaultConfigFile, "config file (JSON or YAML)")
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -37,5 +37,5 @@ func setVerbosity(_ *cobra.Command, _ []string) {
|
|||||||
if desired > 6 {
|
if desired > 6 {
|
||||||
desired = 6
|
desired = 6
|
||||||
}
|
}
|
||||||
log = log.WithLevel(slog.LogLevel(desired))
|
log = zerolog.New(nil, slog.LogLevel(desired))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ type Cluster struct {
|
|||||||
Domain string `json:"domain,omitempty" yaml:"domain,omitempty"`
|
Domain string `json:"domain,omitempty" yaml:"domain,omitempty"`
|
||||||
|
|
||||||
CephFSID uuid.UUID `json:"ceph_fsid,omitempty" yaml:"ceph_fsid,omitempty"`
|
CephFSID uuid.UUID `json:"ceph_fsid,omitempty" yaml:"ceph_fsid,omitempty"`
|
||||||
Zones []*Zone `json:"zones,omitempty" yaml:"zones,omitempty"`
|
Regions []Region `json:",omitempty" yaml:",omitempty"`
|
||||||
|
Zones []*Zone `json:",omitempty" yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// revive:enable:line-length-limit
|
// revive:enable:line-length-limit
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hack-pad/hackpadfs/os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DirFS returns a file system (an [fs.FS]) for the tree
|
||||||
|
// of files rooted at the directory dir.
|
||||||
|
func DirFS(dir string) (fs.FS, error) {
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
fullPath, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := os.NewFS().Sub(fullPath[1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Cluster) init(opts *ScanOptions) error {
|
||||||
|
for _, fn := range []func(*ScanOptions) error{
|
||||||
|
m.initZones,
|
||||||
|
m.scanZoneIDs,
|
||||||
|
m.scanSort,
|
||||||
|
m.scanGateways,
|
||||||
|
m.initRegions,
|
||||||
|
} {
|
||||||
|
if err := fn(opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cluster) initZones(opts *ScanOptions) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
sub, err := DirFS(m.BaseDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dir = sub
|
||||||
|
|
||||||
|
m.ForEachZone(func(z *Zone) bool {
|
||||||
|
err = m.initZone(z, opts)
|
||||||
|
return err != nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cluster) initZone(z *Zone, _ *ScanOptions) error {
|
||||||
|
var hasMissing bool
|
||||||
|
var lastMachineID int
|
||||||
|
|
||||||
|
z.zones = m
|
||||||
|
z.logger = m
|
||||||
|
|
||||||
|
z.ForEachMachine(func(p *Machine) bool {
|
||||||
|
p.zone = z
|
||||||
|
p.logger = z
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case p.ID == 0:
|
||||||
|
hasMissing = true
|
||||||
|
case p.ID > lastMachineID:
|
||||||
|
lastMachineID = z.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if hasMissing {
|
||||||
|
next := lastMachineID + 1
|
||||||
|
|
||||||
|
z.ForEachMachine(func(p *Machine) bool {
|
||||||
|
if p.ID == 0 {
|
||||||
|
p.ID, next = next, next+1
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
z.ForEachMachine(func(p *Machine) bool {
|
||||||
|
p.Name = fmt.Sprintf("%s-%v", z.Name, p.ID)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeConfigData(data []byte) (out *Cluster, err error) {
|
||||||
|
// try JSON first
|
||||||
|
out = new(Cluster)
|
||||||
|
err = json.Unmarshal(data, out)
|
||||||
|
if err == nil {
|
||||||
|
// good json
|
||||||
|
return out, nil
|
||||||
|
} else if _, ok := err.(*json.SyntaxError); !ok {
|
||||||
|
// bad json
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out = new(Cluster)
|
||||||
|
err = yaml.Unmarshal(data, out)
|
||||||
|
if err != nil {
|
||||||
|
// bad yaml too
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// good yaml
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromConfig loads the cluster data from the given file
|
||||||
|
func NewFromConfig(filename string, opts ...ScanOption) (*Cluster, error) {
|
||||||
|
var scanOptions ScanOptions
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := decodeConfigData(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err = opt(m, &scanOptions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = m.setScanDefaults(&scanOptions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.init(&scanOptions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"darvaza.org/resolver"
|
"darvaza.org/resolver"
|
||||||
"darvaza.org/slog"
|
"darvaza.org/slog"
|
||||||
"github.com/hack-pad/hackpadfs/os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A ScanOption pre-configures the Zones before scanning
|
// A ScanOption pre-configures the Zones before scanning
|
||||||
@@ -101,7 +100,7 @@ func NewFromDirectory(dir, domain string, opts ...ScanOption) (*Cluster, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := os.NewFS().Sub(fullPath[1:])
|
sub, err := DirFS(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
// Region represents a group of zones geographically related
|
||||||
|
type Region struct {
|
||||||
|
m *Cluster
|
||||||
|
zones []*Zone
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Regions []string `json:",omitempty" yaml:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cluster) initRegions(_ *ScanOptions) error {
|
||||||
|
regions := make(map[string][]*Zone)
|
||||||
|
|
||||||
|
// first regions defined by zones
|
||||||
|
m.ForEachZone(func(z *Zone) bool {
|
||||||
|
for _, region := range z.Regions {
|
||||||
|
regions[region] = append(regions[region], z)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// bind first level regions and their zones
|
||||||
|
for name, zones := range regions {
|
||||||
|
m.syncRegions(name, zones...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// and combine zones to produce larger regions
|
||||||
|
for i := range m.Regions {
|
||||||
|
r := &m.Regions[i]
|
||||||
|
m.finishRegion(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cluster) syncRegions(name string, zones ...*Zone) {
|
||||||
|
for _, r := range m.Regions {
|
||||||
|
if r.Name == name {
|
||||||
|
// found
|
||||||
|
r.m = m
|
||||||
|
r.zones = zones
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// new
|
||||||
|
m.Regions = append(m.Regions, Region{
|
||||||
|
m: m,
|
||||||
|
zones: zones,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cluster) finishRegion(r *Region) {
|
||||||
|
if r.m != nil {
|
||||||
|
// ready
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.m = m
|
||||||
|
sub := []string{}
|
||||||
|
for _, name := range r.Regions {
|
||||||
|
r2, ok := m.getRegion(name)
|
||||||
|
if !ok {
|
||||||
|
m.warn(nil).WithField("region", name).Print("unknown region")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sub = append(sub, r2.Name)
|
||||||
|
r.zones = append(r.zones, r2.zones...)
|
||||||
|
}
|
||||||
|
r.Regions = sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cluster) getRegion(name string) (*Region, bool) {
|
||||||
|
for i := range m.Regions {
|
||||||
|
r := &m.Regions[i]
|
||||||
|
|
||||||
|
if name == r.Name {
|
||||||
|
m.finishRegion(r)
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
@@ -19,8 +19,9 @@ type Zone struct {
|
|||||||
zones *Cluster
|
zones *Cluster
|
||||||
logger `json:"-" yaml:"-"`
|
logger `json:"-" yaml:"-"`
|
||||||
|
|
||||||
ID int
|
ID int
|
||||||
Name string
|
Name string
|
||||||
|
Regions []string `json:",omitempty" yaml:",omitempty"`
|
||||||
|
|
||||||
Machines
|
Machines
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user