Compare commits

...

38 Commits

Author SHA1 Message Date
amery 4e2693b12c Merge pull request 'dns: introduce DNS Manager and BIND config writer' (#24)
Reviewed-on: #24
2023-09-12 18:22:52 +02:00
amery eba0340e32 jpictl: introduce jpictl dns write command
it renders BIND config to describe the public view of the cluster

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 16:21:23 +00:00
amery 1a47985bd7 dns: Manager.WriteTo() generates BIND config, fully qualifies
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:56:07 +00:00
amery f5ea72740c dns: introduce initial DNS Manager
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:56:07 +00:00
amery 357c85dc1a dns: SortRegions() by ISO3166
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery 00aec477a4 dns: DefaultDNSProvider() using CLOUDFLARE_DNS_API_TOKEN
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery e0d8592dc1 dns: introduce AddrRecord{} to abstract A/AAAA entries
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery c397ca29ac cluster: introduce Region interators
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery 066788b9be vscode: add Lookuper, publicsuffix and libdns to the dictionary
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 15:55:38 +00:00
amery 4402555f04 Merge pull request 'cluster: ensure ceph monitors are set when loading a config file' (#23)
Reviewed-on: #23
2023-09-12 15:21:25 +02:00
amery 6e7f24f491 cluster: ensure ceph monitors are set when loading a config file
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 12:47:24 +00:00
amery 54b302c6d5 vscode: add asciigoat, cyclomatic and Wrapf to the dictionary
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 12:47:18 +00:00
amery f62a47003d Merge pull request 'cluster: introduce Regions to group zones' (#22)
Reviewed-on: #22
2023-09-12 14:45:01 +02:00
amery 5abaed9047 Merge pull request 'jpictl: fix verbosity handling' (#21)
Reviewed-on: #21
2023-09-12 14:43:40 +02:00
amery c702d649e0 cluster: introduce Regions to group zones
only available via config-file

Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 02:17:50 +00:00
amery e9f9d474dc jpictl: fix verbosity handling
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-12 01:38:12 +00:00
amery e2941cf2c0 Merge pull request 'jpictl: introduce --config-file/-f as alternative to scanning m/' (#19)
Reviewed-on: #19
2023-09-11 23:44:39 +02:00
amery ea755113a8 Merge pull request 'hosts: update all hosts files on jpictl write' (#20)
Reviewed-on: #20
2023-09-11 23:42:48 +02:00
amery 1c199ed923 jpictl: update all hosts files on jpictl write
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:57:26 +00:00
amery 5dc5c95aa1 hosts: add generators for hosts files
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:56:52 +00:00
amery a0cc698a39 jpictl: introduce --config-file/-f as alternative to scanning m/
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:43:42 +00:00
amery 70008e0ead cluster: NewFromConfig() trying JSON and YAML
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:43:42 +00:00
amery ec2b30c1e7 cluster: add DirFS() using hackpadfs/os
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:43:42 +00:00
amery 3de7fcb605 Merge pull request 'move pkg/zones to pkg/cluster and other rearrangements' (#18)
Reviewed-on: #18
2023-09-11 20:41:25 +02:00
amery d0b0698c10 cluster: make domain, basedir and cluster name public fields
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:10:47 +00:00
amery 2a2e6c121e cluster: group default option factories
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:10:47 +00:00
amery 24059dc9ee cluster: rename Zones to Cluster
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:10:47 +00:00
amery bedf62977f cluster: move Machines to a dedicated file
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:10:47 +00:00
amery 5abb4c2f92 cluster: rename pkg/zones to pkg/cluster
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-11 18:10:46 +00:00
amery 046c9a508b Merge pull request 'jpictl: fixup the verbosity flag to obey line limit' (#17)
Reviewed-on: #17
2023-09-11 17:47:11 +02:00
Nagy Károly Gábriel f6766547f9 jpictl: fixup the verbosity flag to obey line limit
Signed-off-by: Nagy Károly Gábriel <k@jpi.io>
2023-09-11 18:44:05 +03:00
amery 6aec17d079 Merge pull request 'drop toml support' (#16)
Reviewed-on: #16
2023-09-11 15:17:46 +02:00
amery ab8ffdd507 Merge pull request 'zones: warn but not fail when scanning finds unknown monitors' (#13)
Reviewed-on: #13
2023-09-11 15:15:19 +02:00
amery 422e119f88 Merge pull request 'zones: add structured logs to zone scanning' (#14)
Reviewed-on: #14
2023-09-11 01:46:09 +02:00
amery 204f3a49a1 drop toml support
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-10 19:53:52 +00:00
Nagy Károly Gábriel d1198328f6 jpictl: introduce log verbosity flag
Signed-off-by: Nagy Károly Gábriel <k@jpi.io>
2023-09-10 13:12:50 +03:00
amery 2016b27707 zones: add structured logs to zone scanning
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-08 12:50:22 +00:00
amery c038ad4431 zones: warn but not fail when scanning finds unknown monitors
Signed-off-by: Alejandro Mery <amery@jpi.io>
2023-09-08 12:49:00 +00:00
41 changed files with 1608 additions and 540 deletions
+6
View File
@@ -1,9 +1,15 @@
{
"cSpell.words": [
"asciigoat",
"ceph",
"cyclomatic",
"darvaza",
"gofrs",
"jpictl",
"libdns",
"Lookuper",
"publicsuffix",
"Wrapf",
"zerolog"
]
}
+44 -7
View File
@@ -1,22 +1,59 @@
package main
import "git.jpi.io/amery/jpictl/pkg/zones"
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
type Config struct {
Base string
Domain string
ConfigFile string
}
var cfg = &Config{
Base: "./m",
Domain: "m.jpi.cloud",
Base: "m",
Domain: "jpi.cloud",
}
// LoadZones loads all zones and machines in the config directory
func (cfg *Config) LoadZones(resolve bool) (*zones.Zones, error) {
return zones.New(cfg.Base, cfg.Domain,
zones.ResolvePublicAddresses(resolve),
zones.WithLogger(log),
// or file
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,
cluster.ResolvePublicAddresses(resolve),
cluster.WithLogger(log),
)
}
func init() {
rootCmd.PersistentFlags().
StringVarP(&cfg.ConfigFile, "config-file", "f",
DefaultConfigFile, "config file (JSON or YAML)")
}
+82
View File
@@ -0,0 +1,82 @@
package main
import (
"context"
"os"
"github.com/spf13/cobra"
"git.jpi.io/amery/jpictl/pkg/cluster"
"git.jpi.io/amery/jpictl/pkg/dns"
)
func newDNSManager(m *cluster.Cluster) (*dns.Manager, error) {
ctx := context.TODO()
domain := m.Domain
if m.Name != "" {
domain = m.Name + "." + domain
}
mgr, err := dns.NewManager(dns.WithDomain(domain), dns.WithLogger(log))
if err != nil {
return nil, err
}
m.ForEachZone(func(z *cluster.Zone) bool {
z.ForEachMachine(func(p *cluster.Machine) bool {
err = mgr.AddHost(ctx, z.Name, p.ID, true, p.PublicAddresses...)
return err != nil
})
return err != nil
})
if err != nil {
return nil, err
}
m.ForEachRegion(func(r *cluster.Region) bool {
r.ForEachZone(func(z *cluster.Zone) bool {
err = mgr.AddRegion(ctx, r.Name, z.Name)
return err != nil
})
return err != nil
})
if err != nil {
return nil, err
}
return mgr, nil
}
// Command
var dnsCmd = &cobra.Command{
Use: "dns",
}
var dnsWriteCmd = &cobra.Command{
Use: "write",
Short: "dns write generates public DNS records",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
m, err := cfg.LoadZones(true)
if err != nil {
return err
}
mgr, err := newDNSManager(m)
if err != nil {
return err
}
_, err = mgr.WriteTo(os.Stdout)
return err
},
}
func init() {
rootCmd.AddCommand(dnsCmd)
dnsCmd.AddCommand(dnsWriteCmd)
}
+6 -14
View File
@@ -6,7 +6,6 @@ import (
"io"
"os"
"github.com/burntSushi/toml"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@@ -20,8 +19,8 @@ type Encoder interface {
type Encoding int
const (
// TOMLEncoding represents TOML encoding
TOMLEncoding Encoding = iota
// UndefinedEncoding implies the default encoding
UndefinedEncoding Encoding = iota
// JSONEncoding represents JSON encoding
JSONEncoding
// YAMLEncoding represents YAML encoding
@@ -42,18 +41,13 @@ func NewYAMLEncoder(w io.Writer) Encoder {
return enc
}
// NewTOMLEncoder returns a TOML [Encoder] to work on the given [io.Writer]
func NewTOMLEncoder(w io.Writer) Encoder {
enc := toml.NewEncoder(w)
return enc
}
const encoding = YAMLEncoding
// Command
var dumpCmd = &cobra.Command{
Use: "dump",
Short: "generates a text representation of the config",
Use: "dump",
Short: "generates a text representation of the config",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
var buf bytes.Buffer
var enc Encoder
@@ -66,10 +60,8 @@ var dumpCmd = &cobra.Command{
switch encoding {
case JSONEncoding:
enc = NewJSONEncoder(&buf)
case YAMLEncoding:
enc = NewYAMLEncoder(&buf)
default:
enc = NewTOMLEncoder(&buf)
enc = NewYAMLEncoder(&buf)
}
if err = enc.Encode(m); err != nil {
+3 -2
View File
@@ -8,8 +8,9 @@ import (
// Command
var envCmd = &cobra.Command{
Use: "env",
Short: "generates environment variables for shell scripts",
Use: "env",
Short: "generates environment variables for shell scripts",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
m, err := cfg.LoadZones(false)
if err != nil {
+13 -11
View File
@@ -7,8 +7,9 @@ import (
"strconv"
"strings"
"git.jpi.io/amery/jpictl/pkg/zones"
"github.com/spf13/cobra"
"git.jpi.io/amery/jpictl/pkg/cluster"
)
// Command
@@ -19,8 +20,9 @@ var gatewayCmd = &cobra.Command{
// gateway set
var gatewaySetCmd = &cobra.Command{
Use: "set",
Short: "gateway set sets machines as gateways",
Use: "set",
Short: "gateway set sets machines as gateways",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, args []string) error {
m, err := cfg.LoadZones(false)
if err != nil {
@@ -37,9 +39,9 @@ var gatewaySetCmd = &cobra.Command{
},
}
func gatewaySet(zi zones.ZoneIterator, gw string) error {
func gatewaySet(zi cluster.ZoneIterator, gw string) error {
var err error
zi.ForEachZone(func(z *zones.Zone) bool {
zi.ForEachZone(func(z *cluster.Zone) bool {
for _, m := range z.Machines {
if m.Name == gw {
z.SetGateway(m.ID, true)
@@ -73,9 +75,9 @@ var gatewayUnsetCmd = &cobra.Command{
},
}
func gatewayUnset(zi zones.ZoneIterator, ngw string) error {
func gatewayUnset(zi cluster.ZoneIterator, ngw string) error {
var err error
zi.ForEachZone(func(z *zones.Zone) bool {
zi.ForEachZone(func(z *cluster.Zone) bool {
for _, m := range z.Machines {
if m.Name == ngw && m.IsGateway() {
z.SetGateway(m.ID, false)
@@ -114,10 +116,10 @@ var gatewayListCmd = &cobra.Command{
},
}
func gatewayListAll(zi zones.ZoneIterator) error {
func gatewayListAll(zi cluster.ZoneIterator) error {
var b bytes.Buffer
var err error
zi.ForEachZone(func(z *zones.Zone) bool {
zi.ForEachZone(func(z *cluster.Zone) bool {
b.WriteString(z.Name + ":")
var sIDs []string
ids, num := z.GatewayIDs()
@@ -136,10 +138,10 @@ func gatewayListAll(zi zones.ZoneIterator) error {
return err
}
func gatewayList(zi zones.ZoneIterator, m string) error {
func gatewayList(zi cluster.ZoneIterator, m string) error {
var b bytes.Buffer
var err error
zi.ForEachZone(func(z *zones.Zone) bool {
zi.ForEachZone(func(z *cluster.Zone) bool {
if z.Name == m {
b.WriteString(z.Name + ":")
ids, num := z.GatewayIDs()
-6
View File
@@ -3,15 +3,9 @@ package main
import (
"fmt"
"darvaza.org/sidecar/pkg/logger/zerolog"
"darvaza.org/slog"
)
// TODO: make log level configurable via flags
var (
log = zerolog.New(nil, slog.Debug)
)
// fatal is a convenience wrapper for slog.Logger.Fatal().Print()
func fatal(err error, msg string, args ...any) {
l := log.Fatal()
+23 -4
View File
@@ -2,6 +2,8 @@
package main
import (
"darvaza.org/sidecar/pkg/logger/zerolog"
"darvaza.org/slog"
"github.com/spf13/cobra"
)
@@ -10,13 +12,30 @@ const (
CmdName = "jpictl"
)
var rootCmd = &cobra.Command{
Use: CmdName,
Short: "control tool for jpi.cloud",
}
var (
log = zerolog.New(nil, slog.Error)
verbosity int
rootCmd = &cobra.Command{
Use: CmdName,
Short: "control tool for jpi.cloud",
}
)
func main() {
if err := rootCmd.Execute(); err != nil {
fatal(err, "")
}
}
func init() {
rootCmd.PersistentFlags().CountVarP(&verbosity, "verbosity", "v",
"increase the verbosity level to Warn, Info or Debug")
}
func setVerbosity(_ *cobra.Command, _ []string) {
desired := int8(slog.Error) + int8(verbosity)
if desired > 6 {
desired = 6
}
log = zerolog.New(nil, slog.LogLevel(desired))
}
+3 -2
View File
@@ -6,8 +6,9 @@ import (
// Command
var writeCmd = &cobra.Command{
Use: "write",
Short: "rewrites all config files",
Use: "write",
Short: "rewrites all config files",
PreRun: setVerbosity,
RunE: func(_ *cobra.Command, _ []string) error {
m, err := cfg.LoadZones(false)
if err != nil {
+3 -2
View File
@@ -9,12 +9,14 @@ require (
darvaza.org/sidecar v0.0.2
darvaza.org/slog v0.5.3
darvaza.org/slog/handlers/discard v0.4.5
github.com/burntSushi/toml v0.3.1
github.com/gofrs/uuid/v5 v5.0.0
github.com/hack-pad/hackpadfs v0.2.1
github.com/libdns/cloudflare v0.1.0
github.com/libdns/libdns v0.2.1
github.com/mgechev/revive v1.3.3
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.12.0
golang.org/x/net v0.14.0
gopkg.in/gcfg.v1 v1.2.3
gopkg.in/yaml.v3 v3.0.1
)
@@ -40,7 +42,6 @@ require (
github.com/rs/zerolog v1.30.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.12.0 // indirect
+5 -2
View File
@@ -18,8 +18,6 @@ darvaza.org/slog/handlers/zerolog v0.4.5 h1:W4cgGORx4wImr+RL96CWSQGTdkZzKX6YHXPS
darvaza.org/slog/handlers/zerolog v0.4.5/go.mod h1:mCoh/mIl8Nsa6Yu1Um7d7cos6RuEJzgaTXaX5LDRUao=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/burntSushi/toml v0.3.1 h1:Hu1cOEC2qtKULZJCzym5tyA35bZr3HREuolgiAzMlhY=
github.com/burntSushi/toml v0.3.1/go.mod h1:sGTquCpRYr9McuHdv0m6YKIhx8DJGJa4t04/Y9pfSio=
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -38,6 +36,11 @@ github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMI
github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic=
github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+5 -5
View File
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"bytes"
@@ -12,7 +12,7 @@ import (
)
// GetCephFSID returns our Ceph's FSID
func (m *Zones) GetCephFSID() (uuid.UUID, error) {
func (m *Cluster) GetCephFSID() (uuid.UUID, error) {
if core.IsZero(m.CephFSID) {
// generate one
v, err := uuid.NewV4()
@@ -25,7 +25,7 @@ func (m *Zones) GetCephFSID() (uuid.UUID, error) {
}
// GetCephConfig reads the ceph.conf file
func (m *Zones) GetCephConfig() (*ceph.Config, error) {
func (m *Cluster) GetCephConfig() (*ceph.Config, error) {
data, err := m.ReadFile("ceph.conf")
if err != nil {
return nil, err
@@ -36,7 +36,7 @@ func (m *Zones) GetCephConfig() (*ceph.Config, error) {
}
// WriteCephConfig writes the ceph.conf file
func (m *Zones) WriteCephConfig(cfg *ceph.Config) error {
func (m *Cluster) WriteCephConfig(cfg *ceph.Config) error {
f, err := m.CreateTruncFile("ceph.conf")
if err != nil {
return err
@@ -48,7 +48,7 @@ func (m *Zones) WriteCephConfig(cfg *ceph.Config) error {
}
// GenCephConfig prepares a ceph.Config using the cluster information
func (m *Zones) GenCephConfig() (*ceph.Config, error) {
func (m *Cluster) GenCephConfig() (*ceph.Config, error) {
fsid, err := m.GetCephFSID()
if err != nil {
return nil, err
+107
View File
@@ -0,0 +1,107 @@
package cluster
import (
"os"
"darvaza.org/slog"
"git.jpi.io/amery/jpictl/pkg/ceph"
)
type cephScanTODO struct {
names map[string]bool
addrs map[string]bool
}
func (todo *cephScanTODO) checkMachine(p *Machine) bool {
// on ceph all addresses are ring1
ring1, _ := RingOneAddress(p.Zone(), p.ID)
addr := ring1.String()
if _, found := todo.names[p.Name]; found {
// found on the TODO by name
todo.names[p.Name] = true
todo.addrs[addr] = true
return true
}
if _, found := todo.addrs[addr]; found {
// found on the TODO by address
todo.names[p.Name] = true
todo.addrs[addr] = true
return true
}
return false
}
func (todo *cephScanTODO) LogMissing(log slog.Logger) {
for name, found := range todo.names {
if !found {
log.Warn().
WithField("subsystem", "ceph").
WithField("monitor", name).
Print("unknown monitor")
}
}
for addr, found := range todo.addrs {
if !found {
log.Warn().
WithField("subsystem", "ceph").
WithField("monitor", addr).
Print("unknown monitor")
}
}
}
func newCephScanTODO(cfg *ceph.Config) *cephScanTODO {
todo := &cephScanTODO{
names: make(map[string]bool),
addrs: make(map[string]bool),
}
for _, name := range cfg.Global.Monitors {
todo.names[name] = false
}
for _, addr := range cfg.Global.MonitorsAddr {
todo.addrs[addr.String()] = false
}
return todo
}
func (m *Cluster) scanCephMonitors(opts *ScanOptions) error {
cfg, err := m.GetCephConfig()
switch {
case os.IsNotExist(err):
err = nil
case err != nil:
return err
}
if cfg != nil {
// store FSID
m.CephFSID = cfg.Global.FSID
// flag monitors based on config
todo := newCephScanTODO(cfg)
m.ForEachMachine(func(p *Machine) bool {
p.CephMonitor = todo.checkMachine(p)
return false
})
todo.LogMissing(m.log)
}
return m.initCephMonitors(opts)
}
func (m *Cluster) initCephMonitors(_ *ScanOptions) error {
// make sure every zone has one
m.ForEachZone(func(z *Zone) bool {
_ = z.GetCephMonitors()
return false
})
return nil
}
+78
View File
@@ -0,0 +1,78 @@
// Package cluster contains information about the cluster
package cluster
import (
"io/fs"
"darvaza.org/resolver"
"darvaza.org/slog"
"github.com/gofrs/uuid/v5"
)
var (
_ MachineIterator = (*Cluster)(nil)
_ ZoneIterator = (*Cluster)(nil)
)
// revive:disable:line-length-limit
// Cluster represents all zones in a cluster
type Cluster struct {
dir fs.FS
log slog.Logger
resolver resolver.Resolver
BaseDir string `json:"dir,omitempty" yaml:"dir,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Domain string `json:"domain,omitempty" yaml:"domain,omitempty"`
CephFSID uuid.UUID `json:"ceph_fsid,omitempty" yaml:"ceph_fsid,omitempty"`
Regions []Region `json:",omitempty" yaml:",omitempty"`
Zones []*Zone `json:",omitempty" yaml:",omitempty"`
}
// revive:enable:line-length-limit
// ForEachMachine calls a function for each Machine in the cluster
// until instructed to terminate the loop
func (m *Cluster) ForEachMachine(fn func(*Machine) bool) {
m.ForEachZone(func(z *Zone) bool {
var term bool
z.ForEachMachine(func(p *Machine) bool {
term = fn(p)
return term
})
return term
})
}
// ForEachZone calls a function for each Zone in the cluster
// until instructed to terminate the loop
func (m *Cluster) ForEachZone(fn func(*Zone) bool) {
for _, p := range m.Zones {
if fn(p) {
// terminate
return
}
}
}
// GetMachineByName looks for a machine with the specified
// name on any zone
func (m *Cluster) GetMachineByName(name string) (*Machine, bool) {
var out *Machine
if name != "" {
m.ForEachMachine(func(p *Machine) bool {
if p.Name == name {
out = p
}
return out != nil
})
}
return out, out != nil
}
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"fmt"
@@ -9,7 +9,7 @@ import (
)
// OpenFile opens a file on the cluster's config directory with the specified flags
func (m *Zones) OpenFile(name string, flags int, args ...any) (fs.File, error) {
func (m *Cluster) OpenFile(name string, flags int, args ...any) (fs.File, error) {
if len(args) > 0 {
name = fmt.Sprintf(name, args...)
}
@@ -18,16 +18,16 @@ func (m *Zones) OpenFile(name string, flags int, args ...any) (fs.File, error) {
}
// CreateTruncFile creates or truncates a file on the cluster's config directory
func (m *Zones) CreateTruncFile(name string, args ...any) (io.WriteCloser, error) {
func (m *Cluster) CreateTruncFile(name string, args ...any) (io.WriteCloser, error) {
return m.openWriter(name, os.O_CREATE|os.O_TRUNC, args...)
}
// CreateFile creates a file on the cluster's config directory
func (m *Zones) CreateFile(name string, args ...any) (io.WriteCloser, error) {
func (m *Cluster) CreateFile(name string, args ...any) (io.WriteCloser, error) {
return m.openWriter(name, os.O_CREATE, args...)
}
func (m *Zones) openWriter(name string, flags int, args ...any) (io.WriteCloser, error) {
func (m *Cluster) openWriter(name string, flags int, args ...any) (io.WriteCloser, error) {
f, err := m.OpenFile(name, os.O_WRONLY|flags, args...)
if err != nil {
return nil, err
@@ -37,7 +37,7 @@ func (m *Zones) openWriter(name string, flags int, args ...any) (io.WriteCloser,
}
// ReadFile reads a file from the cluster's config directory
func (m *Zones) ReadFile(name string, args ...any) ([]byte, error) {
func (m *Cluster) ReadFile(name string, args ...any) ([]byte, error) {
if len(args) > 0 {
name = fmt.Sprintf(name, args...)
}
+25
View File
@@ -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
}
+139
View File
@@ -0,0 +1,139 @@
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.initCephMonitors,
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
}
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"io/fs"
@@ -7,7 +7,7 @@ import (
"darvaza.org/core"
)
func (m *Zones) scan(opts *ScanOptions) error {
func (m *Cluster) scan(opts *ScanOptions) error {
for _, fn := range []func(*ScanOptions) error{
m.scanDirectory,
m.scanMachines,
@@ -24,7 +24,7 @@ func (m *Zones) scan(opts *ScanOptions) error {
return nil
}
func (m *Zones) scanDirectory(_ *ScanOptions) error {
func (m *Cluster) scanDirectory(_ *ScanOptions) error {
// each directory is a zone
entries, err := fs.ReadDir(m.dir, ".")
if err != nil {
@@ -37,7 +37,11 @@ func (m *Zones) scanDirectory(_ *ScanOptions) error {
switch {
case err != nil:
return core.Wrap(err, e.Name())
case z.Machines.Len() > 0:
case z.Machines.Len() == 0:
z.warn(nil).
WithField("zone", z.Name).
Print("empty")
default:
m.Zones = append(m.Zones, z)
}
}
@@ -46,20 +50,24 @@ func (m *Zones) scanDirectory(_ *ScanOptions) error {
return nil
}
func (m *Zones) newZone(name string) (*Zone, error) {
func (m *Cluster) newZone(name string) (*Zone, error) {
z := &Zone{
zones: m,
logger: m,
Name: name,
}
z.debug().
WithField("zone", z.Name).
Print("found")
if err := z.scan(); err != nil {
return nil, err
}
return z, nil
}
func (m *Zones) scanMachines(opts *ScanOptions) error {
func (m *Cluster) scanMachines(opts *ScanOptions) error {
var err error
m.ForEachMachine(func(p *Machine) bool {
err = p.scan(opts)
@@ -68,7 +76,7 @@ func (m *Zones) scanMachines(opts *ScanOptions) error {
return err
}
func (m *Zones) scanZoneIDs(_ *ScanOptions) error {
func (m *Cluster) scanZoneIDs(_ *ScanOptions) error {
var hasMissing bool
var lastZoneID int
@@ -98,7 +106,7 @@ func (m *Zones) scanZoneIDs(_ *ScanOptions) error {
return nil
}
func (m *Zones) scanSort(_ *ScanOptions) error {
func (m *Cluster) scanSort(_ *ScanOptions) error {
sort.SliceStable(m.Zones, func(i, j int) bool {
id1 := m.Zones[i].ID
id2 := m.Zones[j].ID
@@ -124,7 +132,7 @@ func (m *Zones) scanSort(_ *ScanOptions) error {
return nil
}
func (m *Zones) scanGateways(_ *ScanOptions) error {
func (m *Cluster) scanGateways(_ *ScanOptions) error {
var err error
m.ForEachZone(func(z *Zone) bool {
@@ -149,7 +157,17 @@ func (z *Zone) scan() error {
Name: e.Name(),
}
m.debug().
WithField("node", m.Name).
WithField("zone", z.Name).
Print("found")
if err := m.init(); err != nil {
m.error(err).
WithField("node", m.Name).
WithField("zone", z.Name).
Print()
return err
}
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"io/fs"
@@ -6,12 +6,10 @@ import (
"darvaza.org/resolver"
"darvaza.org/slog"
"darvaza.org/slog/handlers/discard"
"github.com/hack-pad/hackpadfs/os"
)
// A ScanOption pre-configures the Zones before scanning
type ScanOption func(*Zones, *ScanOptions) error
type ScanOption func(*Cluster, *ScanOptions) error
// ScanOptions contains flags used by the initial scan
type ScanOptions struct {
@@ -29,7 +27,7 @@ type ScanOptions struct {
// the DNS resolver to get PublicAddresses of nodes.
// Default is true
func ResolvePublicAddresses(resolve bool) ScanOption {
return func(m *Zones, opt *ScanOptions) error {
return func(m *Cluster, opt *ScanOptions) error {
opt.DontResolvePublicAddresses = !resolve
return nil
}
@@ -38,7 +36,7 @@ func ResolvePublicAddresses(resolve bool) ScanOption {
// WithLookuper specifies what resolver.Lookuper to use to
// find public addresses
func WithLookuper(h resolver.Lookuper) ScanOption {
return func(m *Zones, opt *ScanOptions) error {
return func(m *Cluster, opt *ScanOptions) error {
if h == nil {
return fs.ErrInvalid
}
@@ -51,7 +49,7 @@ func WithLookuper(h resolver.Lookuper) ScanOption {
// public addresses. if nil is passed, the [net.Resolver] will be used.
// The default is using Cloudflare's 1.1.1.1.
func WithResolver(h resolver.Resolver) ScanOption {
return func(m *Zones, opt *ScanOptions) error {
return func(m *Cluster, opt *ScanOptions) error {
if h == nil {
h = resolver.SystemResolver(true)
}
@@ -63,9 +61,9 @@ func WithResolver(h resolver.Resolver) ScanOption {
// WithLogger specifies what to use for logging
func WithLogger(log slog.Logger) ScanOption {
return func(m *Zones, opt *ScanOptions) error {
return func(m *Cluster, opt *ScanOptions) error {
if log == nil {
log = discard.New()
log = DefaultLogger()
}
opt.Logger = log
@@ -74,9 +72,9 @@ func WithLogger(log slog.Logger) ScanOption {
}
}
func (m *Zones) setDefaults(opt *ScanOptions) error {
func (m *Cluster) setScanDefaults(opt *ScanOptions) error {
if m.resolver == nil {
h := resolver.NewCloudflareLookuper()
h := DefaultLookuper()
if err := WithLookuper(h)(m, opt); err != nil {
return err
@@ -92,43 +90,41 @@ func (m *Zones) setDefaults(opt *ScanOptions) error {
return nil
}
// NewFS builds a [Zones] tree using the given directory
func NewFS(dir fs.FS, domain string, opts ...ScanOption) (*Zones, error) {
// NewFromDirectory builds a [Cluster] tree using the given directory
func NewFromDirectory(dir, domain string, opts ...ScanOption) (*Cluster, error) {
var scanOptions ScanOptions
z := &Zones{
dir: dir,
domain: domain,
dir = filepath.Clean(dir)
fullPath, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
sub, err := DirFS(dir)
if err != nil {
return nil, err
}
m := &Cluster{
dir: sub,
BaseDir: dir,
Name: filepath.Base(fullPath),
Domain: domain,
}
for _, opt := range opts {
if err := opt(z, &scanOptions); err != nil {
if err := opt(m, &scanOptions); err != nil {
return nil, err
}
}
if err := z.setDefaults(&scanOptions); err != nil {
if err := m.setScanDefaults(&scanOptions); err != nil {
return nil, err
}
if err := z.scan(&scanOptions); err != nil {
if err := m.scan(&scanOptions); err != nil {
return nil, err
}
return z, nil
}
// New builds a [Zones] tree using the given directory
func New(dir, domain string, opts ...ScanOption) (*Zones, error) {
dir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
base, err := os.NewFS().Sub(dir[1:])
if err != nil {
return nil, err
}
return NewFS(base, domain, opts...)
return m, nil
}
+17
View File
@@ -0,0 +1,17 @@
package cluster
import (
"darvaza.org/resolver"
"darvaza.org/slog"
"darvaza.org/slog/handlers/discard"
)
// DefaultLogger returns a logger that doesn't log anything
func DefaultLogger() slog.Logger {
return discard.New()
}
// DefaultLookuper returns a [resolver.Lookuper] using Cloudflare's 1.1.1.1
func DefaultLookuper() resolver.Lookuper {
return resolver.NewCloudflareLookuper()
}
+2 -2
View File
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"bytes"
@@ -16,7 +16,7 @@ type Env struct {
}
// Env returns a shell environment factory
func (m *Zones) Env(export bool) (*Env, error) {
func (m *Cluster) Env(export bool) (*Env, error) {
fsid, err := m.GetCephFSID()
if err != nil {
return nil, err
@@ -1,4 +1,4 @@
package zones
package cluster
import "errors"
+128
View File
@@ -0,0 +1,128 @@
package cluster
import (
"bytes"
"fmt"
"strings"
"text/template"
)
type hostsFile struct {
Ring0 []hostsEntry
Ring1 []hostsEntry
}
type hostsEntry struct {
Addr string
Names []string
}
var hostsTemplate = template.Must(template.New("hosts").Funcs(template.FuncMap{
"StringsJoin": strings.Join,
}).Parse(`127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
{{range .Ring1 -}}
{{.Addr}} {{StringsJoin .Names " "}}
{{end}}
{{range .Ring0 -}}
{{.Addr}} {{StringsJoin .Names " "}}
{{end -}}
`))
// WriteHosts rewrites all hosts files on the tree
func (m *Cluster) WriteHosts() error {
var err error
m.ForEachZone(func(z *Zone) bool {
err = z.WriteHosts()
return err != nil
})
return err
}
// WriteHosts rewrites all hosts files in the zone
func (z *Zone) WriteHosts() error {
var err error
s := z.Hosts()
z.ForEachMachine(func(p *Machine) bool {
err = p.WriteStringFile(s, "hosts")
return err != nil
})
return err
}
// WriteHosts rewrites the hosts file
func (p *Machine) WriteHosts() error {
s := p.zone.Hosts()
return p.WriteStringFile(s, "hosts")
}
func (z *Zone) genHosts(out *hostsFile, p *Machine) {
var names []string
ip, _ := RingOneAddress(p.zone.ID, p.ID)
names = append(names, p.Name)
if p.CephMonitor {
names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "ceph"))
names = append(names, fmt.Sprintf("%s-%s", p.zone.Name, "k3s"))
if z.ID == p.zone.ID {
names = append(names, "ceph")
names = append(names, "k3s")
}
}
entry := hostsEntry{
Addr: ip.String(),
Names: names,
}
out.Ring1 = append(out.Ring1, entry)
if p.IsGateway() {
var s string
ip, _ = RingZeroAddress(p.zone.ID, p.ID)
s = fmt.Sprintf("%s-%v", p.Name, 0)
entry = hostsEntry{
Addr: ip.String(),
Names: []string{s},
}
out.Ring0 = append(out.Ring0, entry)
}
}
// Hosts renders the /etc/hosts to be used on this zone
func (z *Zone) Hosts() string {
var buf bytes.Buffer
var out hostsFile
z.zones.ForEachZone(func(z2 *Zone) bool {
z2.ForEachMachine(func(p *Machine) bool {
z.genHosts(&out, p)
return false
})
return false
})
if err := hostsTemplate.Execute(&buf, &out); err != nil {
panic(err)
}
return buf.String()
}
+8 -8
View File
@@ -1,4 +1,4 @@
package zones
package cluster
import "darvaza.org/slog"
@@ -13,26 +13,26 @@ type logger interface {
}
var (
_ logger = (*Zones)(nil)
_ logger = (*Cluster)(nil)
)
func (z *Zones) withDebug() (slog.Logger, bool) {
func (z *Cluster) withDebug() (slog.Logger, bool) {
return z.debug().WithEnabled()
}
func (z *Zones) withInfo() (slog.Logger, bool) {
func (z *Cluster) withInfo() (slog.Logger, bool) {
return z.debug().WithEnabled()
}
func (z *Zones) debug() slog.Logger {
func (z *Cluster) debug() slog.Logger {
return z.log.Debug()
}
func (z *Zones) info() slog.Logger {
func (z *Cluster) info() slog.Logger {
return z.log.Info()
}
func (z *Zones) warn(err error) slog.Logger {
func (z *Cluster) warn(err error) slog.Logger {
l := z.log.Warn()
if err != nil {
l = l.WithField(slog.ErrorFieldName, err)
@@ -40,7 +40,7 @@ func (z *Zones) warn(err error) slog.Logger {
return l
}
func (z *Zones) error(err error) slog.Logger {
func (z *Cluster) error(err error) slog.Logger {
l := z.log.Error()
if err != nil {
l = l.WithField(slog.ErrorFieldName, err)
+17 -15
View File
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"net/netip"
@@ -10,15 +10,14 @@ import (
// A Machine is a machine on a Zone
type Machine struct {
zone *Zone
logger `toml:"-" json:"-" yaml:"-"`
logger `json:"-" yaml:"-"`
ID int `toml:"id"`
Name string `toml:"-" json:"-" yaml:"-"`
ID int
Name string `json:"-" yaml:"-"`
PublicAddresses []netip.Addr `toml:"public,omitempty" json:"public,omitempty" yaml:"public,omitempty"`
Rings []*RingInfo `toml:"rings,omitempty" json:"rings,omitempty" yaml:"rings,omitempty"`
CephMonitor bool `toml:"ceph_monitor,omitempty" json:"ceph_monitor,omitempty" yaml:"ceph_monitor,omitempty"`
CephMonitor bool `json:"ceph_monitor,omitempty" yaml:"ceph_monitor,omitempty"`
PublicAddresses []netip.Addr `json:"public,omitempty" yaml:"public,omitempty"`
Rings []*RingInfo `json:"rings,omitempty" yaml:"rings,omitempty"`
}
// revive:enable:line-length-limit
@@ -29,16 +28,19 @@ func (m *Machine) String() string {
// FullName returns the Name of the machine including domain name
func (m *Machine) FullName() string {
if domain := m.zone.zones.domain; domain != "" {
var s = []string{
m.Name,
domain,
}
var name []string
return strings.Join(s, ".")
for _, s := range []string{
m.Name,
m.zone.zones.Name,
m.zone.zones.Domain,
} {
if s != "" {
name = append(name, s)
}
}
return m.Name
return strings.Join(name, ".")
}
// IsGateway tells if the Machine is a ring0 gateway
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"bytes"
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"bytes"
@@ -122,17 +122,15 @@ func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error {
addr := wg.GetAddress()
zoneID, nodeID, ok := Rings[ring].Decode(addr)
if !ok {
return fmt.Errorf("%s: invalid wg%v address: %s", m.Name, ring, addr)
return fmt.Errorf("%s: invalid address", addr)
}
if err := m.applyZoneNodeID(zoneID, nodeID); err != nil {
err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
return err
return core.Wrapf(err, "%s: invalid address", addr)
}
if err := m.applyWireguardInterfaceConfig(ring, wg.Interface); err != nil {
err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
return err
return core.Wrap(err, "interface")
}
for _, peer := range wg.Peer {
@@ -140,9 +138,14 @@ func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error {
switch {
case errors.Is(err, ErrUnknownNode):
// ignore unknown peers
m.warn(nil).
WithField("subsystem", "wireguard").
WithField("node", m.Name).
WithField("peer", peer.Endpoint.Host).
WithField("ring", ring).
Print("ignoring unknown endpoint")
case err != nil:
err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
return err
return core.Wrap(err, "peer")
}
}
@@ -163,6 +166,10 @@ func (m *Machine) applyRingInfo(ring int, new *RingInfo) error {
cur, _ := m.getRingInfo(ring)
if cur == nil {
// first, append
m.debug().
WithField("node", m.Name).
WithField("ring", ring).
Print("found")
m.Rings = append(m.Rings, new)
return nil
}
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"context"
@@ -71,6 +71,11 @@ func (m *Machine) setID() error {
func (m *Machine) scan(opts *ScanOptions) error {
for i := 0; i < RingsCount; i++ {
if err := m.tryApplyWireguardConfig(i); err != nil {
m.error(err).
WithField("subsystem", "wireguard").
WithField("node", m.Name).
WithField("ring", i).
Print()
return err
}
}
+69
View File
@@ -0,0 +1,69 @@
package cluster
import "sort"
var (
_ MachineIterator = Machines(nil)
_ sort.Interface = Machines(nil)
)
// A MachineIterator is a set of Machines we can iterate on
type MachineIterator interface {
ForEachMachine(func(*Machine) bool)
}
// Machines is a list of Machine objects
type Machines []*Machine
// ForEachMachine calls a function for each Machine in the list
// until instructed to terminate the loop
func (m Machines) ForEachMachine(fn func(*Machine) bool) {
for _, p := range m {
if fn(p) {
return
}
}
}
// Len returns the number of machines in the list
func (m Machines) Len() int {
return len(m)
}
// Less implements sort.Interface to sort the list
func (m Machines) Less(i, j int) bool {
a, b := m[i], m[j]
za, zb := a.Zone(), b.Zone()
switch {
case za == zb:
return a.ID < b.ID
default:
return za < zb
}
}
// Swap implements sort.Interface to sort the list
func (m Machines) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
// FilterMachines produces a subset of the machines offered by the given
// iterator fulfilling a condition
func FilterMachines(m MachineIterator, cond func(*Machine) bool) (Machines, int) {
var out []*Machine
if cond == nil {
// unconditional
cond = func(*Machine) bool { return true }
}
m.ForEachMachine(func(p *Machine) bool {
if cond(p) {
out = append(out, p)
}
return false
})
return out, len(out)
}
+130
View File
@@ -0,0 +1,130 @@
package cluster
var (
_ MachineIterator = (*Region)(nil)
_ ZoneIterator = (*Region)(nil)
)
// Region represents a group of zones geographically related
type Region struct {
m *Cluster
zones []*Zone
Name string
Regions []string `json:",omitempty" yaml:",omitempty"`
}
// ForEachRegion calls a function for each Region of the cluster
// until instructed to terminate the loop
func (m *Cluster) ForEachRegion(fn func(r *Region) bool) {
for i := range m.Regions {
r := &m.Regions[i]
if fn(r) {
return
}
}
}
// ForEachMachine calls a function for each Machine in the region
// until instructed to terminate the loop
func (r *Region) ForEachMachine(fn func(*Machine) bool) {
r.ForEachZone(func(z *Zone) bool {
var term bool
z.ForEachMachine(func(p *Machine) bool {
term = fn(p)
return term
})
return term
})
}
// ForEachZone calls a function for each Zone in the region
// until instructed to terminate the loop
func (r *Region) ForEachZone(fn func(*Zone) bool) {
for _, p := range r.zones {
if fn(p) {
// terminate
return
}
}
}
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
}
+4 -4
View File
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"fmt"
@@ -24,9 +24,9 @@ const (
// RingInfo contains represents the Wireguard endpoint details
// for a Machine on a particular ring
type RingInfo struct {
Ring int `toml:"ring"`
Enabled bool `toml:"enabled,omitempty"`
Keys wireguard.KeyPair `toml:"keys,omitempty"`
Ring int
Enabled bool
Keys wireguard.KeyPair
}
// Merge attempts to combine two RingInfo structs
+5 -4
View File
@@ -1,10 +1,11 @@
package zones
package cluster
// SyncAll updates all config files
func (m *Zones) SyncAll() error {
func (m *Cluster) SyncAll() error {
for _, fn := range []func() error{
m.SyncAllWireguard,
m.SyncAllCeph,
m.WriteHosts,
} {
if err := fn(); err != nil {
return err
@@ -15,7 +16,7 @@ func (m *Zones) SyncAll() error {
}
// SyncAllWireguard updates all wireguard config files
func (m *Zones) SyncAllWireguard() error {
func (m *Cluster) SyncAllWireguard() error {
var err error
for ring := 0; ring < RingsCount; ring++ {
@@ -34,7 +35,7 @@ func (m *Zones) SyncAllWireguard() error {
}
// SyncAllCeph updates the ceph.conf file
func (m *Zones) SyncAllCeph() error {
func (m *Cluster) SyncAllCeph() error {
cfg, err := m.GenCephConfig()
if err != nil {
return err
@@ -1,4 +1,4 @@
package zones
package cluster
import (
"io/fs"
@@ -6,19 +6,19 @@ import (
)
var (
_ WireguardConfigPruner = (*Zones)(nil)
_ WireguardConfigPruner = (*Cluster)(nil)
_ WireguardConfigPruner = (*Zone)(nil)
_ WireguardConfigPruner = (*Machine)(nil)
_ WireguardConfigWriter = (*Zones)(nil)
_ WireguardConfigWriter = (*Cluster)(nil)
_ WireguardConfigWriter = (*Zone)(nil)
_ WireguardConfigWriter = (*Machine)(nil)
_ WireguardConfigSyncer = (*Zones)(nil)
_ WireguardConfigSyncer = (*Cluster)(nil)
_ WireguardConfigSyncer = (*Zone)(nil)
_ WireguardConfigSyncer = (*Machine)(nil)
_ WireguardKeysWriter = (*Zones)(nil)
_ WireguardKeysWriter = (*Cluster)(nil)
_ WireguardKeysWriter = (*Zone)(nil)
_ WireguardKeysWriter = (*Machine)(nil)
)
@@ -31,7 +31,7 @@ type WireguardConfigPruner interface {
// PruneWireguardConfig removes wgN.conf files of machines with
// the corresponding ring disabled on all zones
func (m *Zones) PruneWireguardConfig(ring int) error {
func (m *Cluster) PruneWireguardConfig(ring int) error {
return pruneWireguardConfig(m, ring)
}
@@ -76,7 +76,7 @@ type WireguardConfigWriter interface {
// WriteWireguardConfig rewrites all wgN.conf on all machines
// attached to that ring
func (m *Zones) WriteWireguardConfig(ring int) error {
func (m *Cluster) WriteWireguardConfig(ring int) error {
switch ring {
case 0:
return writeWireguardConfig(m, m, ring)
@@ -154,7 +154,7 @@ type WireguardConfigSyncer interface {
// SyncWireguardConfig updates all wgN.conf files for the specified
// ring
func (m *Zones) SyncWireguardConfig(ring int) error {
func (m *Cluster) SyncWireguardConfig(ring int) error {
switch ring {
case 0:
return syncWireguardConfig(m, m, ring)
@@ -214,7 +214,7 @@ type WireguardKeysWriter interface {
}
// WriteWireguardKeys rewrites all wgN.{key,pub} files
func (m *Zones) WriteWireguardKeys(ring int) error {
func (m *Cluster) WriteWireguardKeys(ring int) error {
return writeWireguardKeys(m, ring)
}
+69
View File
@@ -0,0 +1,69 @@
package cluster
import (
"io/fs"
)
var (
_ MachineIterator = (*Zone)(nil)
)
// A ZoneIterator is a set of Zones we can iterate on
type ZoneIterator interface {
ForEachZone(func(*Zone) bool)
}
// A Zone is a set of machines in close proximity and strong
// affinity.
type Zone struct {
zones *Cluster
logger `json:"-" yaml:"-"`
ID int
Name string
Regions []string `json:",omitempty" yaml:",omitempty"`
Machines
}
func (z *Zone) String() string {
return z.Name
}
// SetGateway configures a machine to be the zone's ring0 gateway
func (z *Zone) SetGateway(gatewayID int, enabled bool) error {
var err error
var found bool
z.ForEachMachine(func(p *Machine) bool {
if p.ID == gatewayID {
found = true
err = p.SetGateway(enabled)
return true
}
return false
})
switch {
case err != nil:
return err
case !found:
return fs.ErrNotExist
default:
return nil
}
}
// GatewayIDs returns the list of IDs of machines that act as ring0 gateways
func (z *Zone) GatewayIDs() ([]int, int) {
var out []int
z.ForEachMachine(func(p *Machine) bool {
if p.IsGateway() {
out = append(out, p.ID)
}
return false
})
return out, len(out)
}
+38
View File
@@ -0,0 +1,38 @@
// Package dns manages DNS entries for the cluster
package dns
import (
"fmt"
"net/netip"
)
// Zone represents a set of hosts with high affinity
type Zone struct {
Name string
Hosts map[int]*Host
}
func (z *Zone) String() string {
if z == nil {
return "undetermined"
}
return z.Name
}
// Host represents a member of the cluster
type Host struct {
zone *Zone
ID int
Active bool
Addrs []netip.Addr
}
func (p *Host) String() string {
if p == nil {
return "undetermined"
}
return fmt.Sprintf("%s-%v", p.zone, p.ID)
}
+173
View File
@@ -0,0 +1,173 @@
package dns
import (
"context"
"errors"
"io/fs"
"net/netip"
"strings"
"darvaza.org/core"
"darvaza.org/slog"
"git.jpi.io/amery/jpictl/pkg/cluster"
"golang.org/x/net/publicsuffix"
)
// Manager is a DNS Manager instance
type Manager struct {
domain string
suffix string
zones map[string]*Zone
regions map[string][]string
p Provider
l slog.Logger
}
// ManagerOption configures a Manager
type ManagerOption func(*Manager) error
func newErrorManagerOption(err error, hint string) ManagerOption {
return func(*Manager) error {
return core.Wrap(err, hint)
}
}
// WithProvider attaches a libdns Provider to the Manager
func WithProvider(p Provider) ManagerOption {
var err error
if p == nil {
p, err = DefaultDNSProvider()
}
if err != nil {
return newErrorManagerOption(err, "WithProvider")
}
return func(mgr *Manager) error {
mgr.p = p
return nil
}
}
// WithLogger attaches a logger to the Manager
func WithLogger(log slog.Logger) ManagerOption {
if log == nil {
log = cluster.DefaultLogger()
}
return func(mgr *Manager) error {
mgr.l = log
return nil
}
}
func (mgr *Manager) setDefaults() error {
var opts []ManagerOption
if mgr.l == nil {
opts = append(opts, WithLogger(nil))
}
if mgr.domain == "" || mgr.suffix == "" {
return errors.New("domain not specified")
}
for _, opt := range opts {
if err := opt(mgr); err != nil {
return err
}
}
return nil
}
// WithDomain specifies where the manager operates
func WithDomain(domain string) ManagerOption {
base, err := publicsuffix.EffectiveTLDPlusOne(domain)
if err != nil {
return newErrorManagerOption(err, "publicsuffix")
}
suffix := strings.TrimSuffix(domain, base)
if suffix != "" {
suffix = "." + suffix[:len(suffix)-1]
}
return func(mgr *Manager) error {
mgr.domain = base
mgr.suffix = suffix
return nil
}
}
// NewManager creates a DNS manager with the
func NewManager(opts ...ManagerOption) (*Manager, error) {
mgr := &Manager{
zones: make(map[string]*Zone),
regions: make(map[string][]string),
}
for _, opt := range opts {
if err := opt(mgr); err != nil {
return nil, err
}
}
if err := mgr.setDefaults(); err != nil {
return nil, err
}
return mgr, nil
}
// AddHost registers a host
func (mgr *Manager) AddHost(_ context.Context, zone string, id int,
active bool, addrs ...netip.Addr) error {
//
if zone == "" || id <= 0 {
return fs.ErrInvalid
}
z, ok := mgr.zones[zone]
if !ok {
z = &Zone{
Name: zone,
Hosts: make(map[int]*Host),
}
mgr.zones[zone] = z
}
p := &Host{
zone: z,
ID: id,
Active: active,
Addrs: SortAddrSlice(addrs),
}
z.Hosts[id] = p
if log, ok := mgr.l.Debug().WithEnabled(); ok {
log.WithField("subsystem", "dns").
WithField("zone", zone).
WithField("host", p.String()).
WithField("active", active).
Print()
}
return nil
}
// AddRegion specifies a new region and the zones it contains
func (mgr *Manager) AddRegion(_ context.Context, region string, zones ...string) error {
mgr.regions[region] = append(mgr.regions[region], zones...)
if log, ok := mgr.l.Debug().WithEnabled(); ok {
for _, zoneName := range zones {
log.WithField("subsystem", "dns").
WithField("region", region).
WithField("zone", zoneName).Print()
}
}
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package dns
import (
"fmt"
"os"
"github.com/libdns/cloudflare"
"github.com/libdns/libdns"
)
const (
// CloudflareAPIToken is the environment variable
// containing the API Token
CloudflareAPIToken = "CLOUDFLARE_DNS_API_TOKEN"
)
// Provider manages DNS entries
type Provider interface {
libdns.RecordGetter
libdns.RecordDeleter
}
// DefaultDNSProvider returns a cloudflare DNS provider
// using an API Token from env [CloudflareAPIToken]
func DefaultDNSProvider() (*cloudflare.Provider, error) {
s := os.Getenv(CloudflareAPIToken)
if s == "" {
return nil, fmt.Errorf("%q: %s", CloudflareAPIToken, "not found")
}
p := &cloudflare.Provider{
APIToken: s,
}
return p, nil
}
+212
View File
@@ -0,0 +1,212 @@
package dns
import (
"bytes"
"fmt"
"io"
"net/netip"
"sort"
"time"
"darvaza.org/core"
"github.com/libdns/libdns"
)
func (mgr *Manager) fqdn(name string) string {
return fmt.Sprintf("%s.%s.", name, mgr.domain)
}
// SortAddrSlice sorts a slice of [netip.Addr]
func SortAddrSlice(s []netip.Addr) []netip.Addr {
sort.Slice(s, func(i, j int) bool {
return s[i].Less(s[j])
})
return s
}
// SortAddrRecords sorts a slice of [AddrRecord]
// by Name and Address
func SortAddrRecords(s []AddrRecord) []AddrRecord {
sort.Slice(s, func(i, j int) bool {
return s[i].Name < s[j].Name
})
for _, p := range s {
SortAddrSlice(p.Addr)
}
return s
}
// SortRegions sorts regions. first by length those 3-character
// or shorter, and then by length. It's mostly aimed at
// supporting ISO-3166 order
func SortRegions(regions []string) []string {
sort.Slice(regions, func(i, j int) bool {
r1, r2 := regions[i], regions[j]
switch {
case len(r1) < 4:
switch {
case len(r1) < len(r2):
return true
case len(r1) > len(r2):
return false
default:
return r1 < r2
}
case len(r2) < 4:
return false
default:
return r1 < r2
}
})
return regions
}
// AddrRecord represents an A or AAAA record
type AddrRecord struct {
Name string
Addr []netip.Addr
}
// Sort sorts the addresses of the record
func (rr *AddrRecord) Sort() {
SortAddrSlice(rr.Addr)
}
// Export converts the record into libdns.Record
func (rr *AddrRecord) Export() []libdns.Record {
out := make([]libdns.Record, len(rr.Addr))
for i, addr := range rr.Addr {
out[i] = libdns.Record{
Name: rr.Name,
TTL: time.Second * 1,
Type: core.IIf(addr.Is6(), "AAAA", "A"),
Value: addr.String(),
}
}
return out
}
// WriteTo writes the record in BIND notation
func (rr *AddrRecord) WriteTo(w io.Writer) (int64, error) {
var total int
for _, addr := range rr.Addr {
n, err := fmt.Fprint(w,
rr.Name, "\t",
1, "\t",
core.IIf(addr.Is6(), "AAAA", "A"), "\t",
addr.String(), "\n")
switch {
case err != nil:
return 0, err
case n > 0:
total += n
}
}
return int64(total), nil
}
// String converts the record into BIND entries
func (rr *AddrRecord) String() string {
var buf bytes.Buffer
_, _ = rr.WriteTo(&buf)
return buf.String()
}
func (mgr *Manager) genRegionsSorted() []string {
regions := make([]string, 0, len(mgr.regions))
for name := range mgr.regions {
regions = append(regions, name)
}
return SortRegions(regions)
}
func (mgr *Manager) genAllAddrRecords() []AddrRecord {
var out []AddrRecord
cache := make(map[string][]netip.Addr)
// zones
for _, z := range mgr.zones {
// hosts
s := mgr.genZoneHostRecords(z)
out = append(out, s...)
// zone alias
addrs := mgr.genZoneAddresses(z)
name := z.Name
out = append(out, AddrRecord{
Name: name + mgr.suffix,
Addr: addrs,
})
// and cache for regions
cache[name] = addrs
}
for _, name := range mgr.genRegionsSorted() {
var addrs []netip.Addr
for _, z := range mgr.regions[name] {
addrs = append(addrs, cache[z]...)
}
rec := AddrRecord{
Name: name + mgr.suffix,
Addr: addrs,
}
rec.Sort()
out = append(out, rec)
}
return out
}
func (*Manager) genZoneAddresses(z *Zone) []netip.Addr {
var out []netip.Addr
for _, p := range z.Hosts {
if p.Active {
out = append(out, p.Addrs...)
}
}
SortAddrSlice(out)
return out
}
func (mgr *Manager) genZoneHostRecords(z *Zone) []AddrRecord {
out := make([]AddrRecord, 0, len(z.Hosts))
for _, p := range z.Hosts {
rec := AddrRecord{
Name: p.String() + mgr.suffix,
Addr: p.Addrs,
}
out = append(out, rec)
}
SortAddrRecords(out)
return out
}
func (mgr *Manager) genRegionAddressesCached(name string,
zones map[string][]netip.Addr) []netip.Addr {
//
var addrs []netip.Addr
for _, zoneName := range mgr.regions[name] {
addrs = append(addrs, zones[zoneName]...)
}
SortAddrSlice(addrs)
return addrs
}
+62
View File
@@ -0,0 +1,62 @@
package dns
import (
"bytes"
"fmt"
"io"
"net/netip"
)
// WriteTo writes the DNS data for the cluster
func (mgr *Manager) WriteTo(w io.Writer) (int64, error) {
var buf bytes.Buffer
cache := make(map[string][]netip.Addr)
// zones
for _, z := range mgr.zones {
mgr.writeZoneHosts(&buf, z)
// zone alias
addrs := mgr.genZoneAddresses(z)
zoneName := z.Name
rr := AddrRecord{
Name: mgr.fqdn(zoneName + mgr.suffix),
Addr: addrs,
}
_, _ = rr.WriteTo(&buf)
// and cache for regions
cache[zoneName] = addrs
}
// regions, sorted
for _, name := range mgr.genRegionsSorted() {
addrs := mgr.genRegionAddressesCached(name, cache)
mgr.writeRegionAddresses(&buf, name, addrs)
}
return buf.WriteTo(w)
}
func (mgr *Manager) writeZoneHosts(w io.Writer, z *Zone) {
_, _ = fmt.Fprintf(w, ";\n; %s\n;\n", z.Name)
for _, rr := range mgr.genZoneHostRecords(z) {
rr.Name = mgr.fqdn(rr.Name)
_, _ = rr.WriteTo(w)
}
}
func (mgr *Manager) writeRegionAddresses(w io.Writer, name string, addrs []netip.Addr) {
_, _ = fmt.Fprintf(w, "; %s\n", name)
rr := AddrRecord{
Name: mgr.fqdn(name + mgr.suffix),
Addr: addrs,
}
_, _ = rr.WriteTo(w)
}
-183
View File
@@ -1,183 +0,0 @@
package zones
import (
"net/netip"
"os"
"strings"
"darvaza.org/core"
"git.jpi.io/amery/jpictl/pkg/ceph"
)
// CephMissingMonitorError is an error that contains ceph
// monitors present in ceph.conf but not found on the cluster
type CephMissingMonitorError struct {
Names []string
Addrs []netip.Addr
}
func (err *CephMissingMonitorError) appendName(name string) {
err.Names = append(err.Names, name)
}
func (err *CephMissingMonitorError) appendAddr(addr netip.Addr) {
err.Addrs = append(err.Addrs, addr)
}
// OK tells if this instance actual shouldn't be treated as an error
func (err CephMissingMonitorError) OK() bool {
switch {
case len(err.Names) > 0:
return false
case len(err.Addrs) > 0:
return false
default:
return true
}
}
func (err CephMissingMonitorError) Error() string {
if !err.OK() {
var buf strings.Builder
_, _ = buf.WriteString("missing:")
err.writeNames(&buf)
err.writeAddrs(&buf)
return buf.String()
}
// no error
return ""
}
func (err *CephMissingMonitorError) writeNames(w *strings.Builder) {
if len(err.Names) > 0 {
_, _ = w.WriteString(" mon_initial_members:")
for i, name := range err.Names {
if i != 0 {
_, _ = w.WriteRune(',')
}
_, _ = w.WriteString(name)
}
}
}
func (err *CephMissingMonitorError) writeAddrs(w *strings.Builder) {
if len(err.Addrs) > 0 {
_, _ = w.WriteString(" mon_host:")
for i, addr := range err.Addrs {
if i != 0 {
_, _ = w.WriteRune(',')
}
_, _ = w.WriteString(addr.String())
}
}
}
// AsError returns nil if the instance is actually OK
func (err *CephMissingMonitorError) AsError() error {
if err == nil || err.OK() {
return nil
}
return err
}
type cephScanTODO struct {
names map[string]bool
addrs map[string]bool
}
func (todo *cephScanTODO) checkMachine(p *Machine) bool {
// on ceph all addresses are ring1
ring1, _ := RingOneAddress(p.Zone(), p.ID)
addr := ring1.String()
if _, found := todo.names[p.Name]; found {
// found on the TODO by name
todo.names[p.Name] = true
todo.addrs[addr] = true
return true
}
if _, found := todo.addrs[addr]; found {
// found on the TODO by address
todo.names[p.Name] = true
todo.addrs[addr] = true
return true
}
return false
}
func (todo *cephScanTODO) Missing() error {
var check CephMissingMonitorError
for name, found := range todo.names {
if !found {
check.appendName(name)
}
}
for addr, found := range todo.addrs {
if !found {
var a netip.Addr
// it wouldn't be on the map if it wasn't valid
_ = a.UnmarshalText([]byte(addr))
check.appendAddr(a)
}
}
return check.AsError()
}
func newCephScanTODO(cfg *ceph.Config) *cephScanTODO {
todo := &cephScanTODO{
names: make(map[string]bool),
addrs: make(map[string]bool),
}
for _, name := range cfg.Global.Monitors {
todo.names[name] = false
}
for _, addr := range cfg.Global.MonitorsAddr {
todo.addrs[addr.String()] = false
}
return todo
}
func (m *Zones) scanCephMonitors(_ *ScanOptions) error {
cfg, err := m.GetCephConfig()
switch {
case os.IsNotExist(err):
err = nil
case err != nil:
return err
}
if cfg != nil {
// store FSID
m.CephFSID = cfg.Global.FSID
// flag monitors based on config
todo := newCephScanTODO(cfg)
m.ForEachMachine(func(p *Machine) bool {
p.CephMonitor = todo.checkMachine(p)
return false
})
if err := todo.Missing(); err != nil {
return core.Wrap(err, "ceph")
}
}
// make sure every zone has one
m.ForEachZone(func(z *Zone) bool {
_ = z.GetCephMonitors()
return false
})
return nil
}
-199
View File
@@ -1,199 +0,0 @@
// Package zones contains information about the cluster
package zones
import (
"io/fs"
"sort"
"darvaza.org/resolver"
"darvaza.org/slog"
"github.com/gofrs/uuid/v5"
)
var (
_ MachineIterator = Machines(nil)
_ sort.Interface = Machines(nil)
_ MachineIterator = (*Zone)(nil)
_ MachineIterator = (*Zones)(nil)
_ ZoneIterator = (*Zones)(nil)
)
// A MachineIterator is a set of Machines we can iterate on
type MachineIterator interface {
ForEachMachine(func(*Machine) bool)
}
// A ZoneIterator is a set of Zones we can iterate on
type ZoneIterator interface {
ForEachZone(func(*Zone) bool)
}
// Machines is a list of Machine objects
type Machines []*Machine
// ForEachMachine calls a function for each Machine in the list
// until instructed to terminate the loop
func (m Machines) ForEachMachine(fn func(*Machine) bool) {
for _, p := range m {
if fn(p) {
return
}
}
}
// Len returns the number of machines in the list
func (m Machines) Len() int {
return len(m)
}
// Less implements sort.Interface to sort the list
func (m Machines) Less(i, j int) bool {
a, b := m[i], m[j]
za, zb := a.Zone(), b.Zone()
switch {
case za == zb:
return a.ID < b.ID
default:
return za < zb
}
}
// Swap implements sort.Interface to sort the list
func (m Machines) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
// FilterMachines produces a subset of the machines offered by the given
// iterator fulfilling a condition
func FilterMachines(m MachineIterator, cond func(*Machine) bool) (Machines, int) {
var out []*Machine
if cond == nil {
// unconditional
cond = func(*Machine) bool { return true }
}
m.ForEachMachine(func(p *Machine) bool {
if cond(p) {
out = append(out, p)
}
return false
})
return out, len(out)
}
// Zone represents one zone in a cluster
type Zone struct {
zones *Zones
logger `toml:"-" json:"-" yaml:"-"`
ID int `toml:"id"`
Name string `toml:"name"`
Machines `toml:"machines"`
}
func (z *Zone) String() string {
return z.Name
}
// SetGateway configures a machine to be the zone's ring0 gateway
func (z *Zone) SetGateway(gatewayID int, enabled bool) error {
var err error
var found bool
z.ForEachMachine(func(p *Machine) bool {
if p.ID == gatewayID {
found = true
err = p.SetGateway(enabled)
return true
}
return false
})
switch {
case err != nil:
return err
case !found:
return fs.ErrNotExist
default:
return nil
}
}
// GatewayIDs returns the list of IDs of machines that act as ring0 gateways
func (z *Zone) GatewayIDs() ([]int, int) {
var out []int
z.ForEachMachine(func(p *Machine) bool {
if p.IsGateway() {
out = append(out, p.ID)
}
return false
})
return out, len(out)
}
// revive:disable:line-length-limit
// Zones represents all zones in a cluster
type Zones struct {
dir fs.FS
log slog.Logger
resolver resolver.Resolver
domain string
CephFSID uuid.UUID `toml:"ceph_fsid,omitempty" json:"ceph_fsid,omitempty" yaml:"ceph_fsid,omitempty"`
Zones []*Zone `toml:"zones"`
}
// revive:enable:line-length-limit
// ForEachMachine calls a function for each Machine in the cluster
// until instructed to terminate the loop
func (m *Zones) ForEachMachine(fn func(*Machine) bool) {
m.ForEachZone(func(z *Zone) bool {
var term bool
z.ForEachMachine(func(p *Machine) bool {
term = fn(p)
return term
})
return term
})
}
// ForEachZone calls a function for each Zone in the cluster
// until instructed to terminate the loop
func (m *Zones) ForEachZone(fn func(*Zone) bool) {
for _, p := range m.Zones {
if fn(p) {
// terminate
return
}
}
}
// GetMachineByName looks for a machine with the specified
// name on any zone
func (m *Zones) GetMachineByName(name string) (*Machine, bool) {
var out *Machine
if name != "" {
m.ForEachMachine(func(p *Machine) bool {
if p.Name == name {
out = p
}
return out != nil
})
}
return out, out != nil
}