Compare commits
12 Commits
v0.6.5
..
3e45a56cd1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e45a56cd1 | |||
| 828d95aa3b | |||
| b4975ae5c5 | |||
| c38dd19a92 | |||
| b24a0c79a0 | |||
| 3dc9645565 | |||
| db3ef6748d | |||
| 24f3648f9d | |||
| bdeac8143d | |||
| db62adfb9c | |||
| 4599eca7d9 | |||
| 312dbe2269 |
Vendored
-3
@@ -1,12 +1,9 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"asciigoat",
|
||||
"ceph",
|
||||
"cyclomatic",
|
||||
"darvaza",
|
||||
"gofrs",
|
||||
"jpictl",
|
||||
"Wrapf",
|
||||
"zerolog"
|
||||
]
|
||||
}
|
||||
|
||||
+1
-38
@@ -1,25 +1,11 @@
|
||||
package main
|
||||
|
||||
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"
|
||||
)
|
||||
import "git.jpi.io/amery/jpictl/pkg/cluster"
|
||||
|
||||
// Config describes the repository
|
||||
type Config struct {
|
||||
Base string
|
||||
Domain string
|
||||
|
||||
ConfigFile string
|
||||
}
|
||||
|
||||
var cfg = &Config{
|
||||
@@ -28,32 +14,9 @@ var cfg = &Config{
|
||||
}
|
||||
|
||||
// LoadZones loads all zones and machines in the config directory
|
||||
// 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)")
|
||||
}
|
||||
|
||||
+2
-3
@@ -45,9 +45,8 @@ const encoding = YAMLEncoding
|
||||
|
||||
// Command
|
||||
var dumpCmd = &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: "generates a text representation of the config",
|
||||
PreRun: setVerbosity,
|
||||
Use: "dump",
|
||||
Short: "generates a text representation of the config",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
var buf bytes.Buffer
|
||||
var enc Encoder
|
||||
|
||||
+2
-3
@@ -8,9 +8,8 @@ import (
|
||||
|
||||
// Command
|
||||
var envCmd = &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "generates environment variables for shell scripts",
|
||||
PreRun: setVerbosity,
|
||||
Use: "env",
|
||||
Short: "generates environment variables for shell scripts",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
m, err := cfg.LoadZones(false)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,9 +20,8 @@ var gatewayCmd = &cobra.Command{
|
||||
|
||||
// gateway set
|
||||
var gatewaySetCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "gateway set sets machines as gateways",
|
||||
PreRun: setVerbosity,
|
||||
Use: "set",
|
||||
Short: "gateway set sets machines as gateways",
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
m, err := cfg.LoadZones(false)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,9 +3,15 @@ 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()
|
||||
|
||||
+4
-23
@@ -2,8 +2,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"darvaza.org/sidecar/pkg/logger/zerolog"
|
||||
"darvaza.org/slog"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -12,30 +10,13 @@ const (
|
||||
CmdName = "jpictl"
|
||||
)
|
||||
|
||||
var (
|
||||
log = zerolog.New(nil, slog.Error)
|
||||
verbosity int
|
||||
rootCmd = &cobra.Command{
|
||||
Use: CmdName,
|
||||
Short: "control tool for jpi.cloud",
|
||||
}
|
||||
)
|
||||
var 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))
|
||||
}
|
||||
|
||||
+2
-3
@@ -6,9 +6,8 @@ import (
|
||||
|
||||
// Command
|
||||
var writeCmd = &cobra.Command{
|
||||
Use: "write",
|
||||
Short: "rewrites all config files",
|
||||
PreRun: setVerbosity,
|
||||
Use: "write",
|
||||
Short: "rewrites all config files",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
m, err := cfg.LoadZones(false)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,7 +14,6 @@ require (
|
||||
github.com/mgechev/revive v1.3.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
golang.org/x/crypto v0.12.0
|
||||
gopkg.in/gcfg.v1 v1.2.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -43,5 +42,4 @@ require (
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
@@ -98,10 +98,6 @@ golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -71,7 +71,7 @@ func newCephScanTODO(cfg *ceph.Config) *cephScanTODO {
|
||||
return todo
|
||||
}
|
||||
|
||||
func (m *Cluster) scanCephMonitors(opts *ScanOptions) error {
|
||||
func (m *Cluster) scanCephMonitors(_ *ScanOptions) error {
|
||||
cfg, err := m.GetCephConfig()
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
@@ -94,10 +94,6 @@ func (m *Cluster) scanCephMonitors(opts *ScanOptions) error {
|
||||
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()
|
||||
|
||||
@@ -27,8 +27,7 @@ type Cluster struct {
|
||||
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"`
|
||||
Zones []*Zone `json:"zones,omitempty" yaml:"zones,omitempty"`
|
||||
}
|
||||
|
||||
// revive:enable:line-length-limit
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"darvaza.org/resolver"
|
||||
"darvaza.org/slog"
|
||||
"github.com/hack-pad/hackpadfs/os"
|
||||
)
|
||||
|
||||
// A ScanOption pre-configures the Zones before scanning
|
||||
@@ -100,7 +101,7 @@ func NewFromDirectory(dir, domain string, opts ...ScanOption) (*Cluster, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sub, err := DirFS(dir)
|
||||
sub, err := os.NewFS().Sub(fullPath[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -5,7 +5,6 @@ func (m *Cluster) SyncAll() error {
|
||||
for _, fn := range []func() error{
|
||||
m.SyncAllWireguard,
|
||||
m.SyncAllCeph,
|
||||
m.WriteHosts,
|
||||
} {
|
||||
if err := fn(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -19,9 +19,8 @@ type Zone struct {
|
||||
zones *Cluster
|
||||
logger `json:"-" yaml:"-"`
|
||||
|
||||
ID int
|
||||
Name string
|
||||
Regions []string `json:",omitempty" yaml:",omitempty"`
|
||||
ID int
|
||||
Name string
|
||||
|
||||
Machines
|
||||
}
|
||||
|
||||
+14
-110
@@ -2,7 +2,6 @@ package wireguard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
@@ -10,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"asciigoat.org/ini/basic"
|
||||
"darvaza.org/core"
|
||||
"gopkg.in/gcfg.v1"
|
||||
)
|
||||
|
||||
var configTemplate = template.Must(template.New("config").Funcs(template.FuncMap{
|
||||
@@ -107,6 +106,11 @@ func (ep EndpointAddress) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalText loads an endpoint address from text data
|
||||
func (ep *EndpointAddress) UnmarshalText(b []byte) error {
|
||||
return ep.FromString(string(b))
|
||||
}
|
||||
|
||||
// FromString sets the EndpointAddress from a given "[host]:port"
|
||||
func (ep *EndpointAddress) FromString(s string) error {
|
||||
host, port, err := core.SplitHostPort(s)
|
||||
@@ -127,98 +131,6 @@ func (ep *EndpointAddress) FromString(s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type intermediateConfig struct {
|
||||
Interface interfaceConfig
|
||||
Peer peersConfig
|
||||
}
|
||||
|
||||
func (v *intermediateConfig) Export() (*Config, error) {
|
||||
var out Config
|
||||
var err error
|
||||
|
||||
// Interface
|
||||
out.Interface, err = v.Interface.Export()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Peers
|
||||
peers, ok := v.PeersCount()
|
||||
if !ok {
|
||||
return nil, errors.New("inconsistent Peer data")
|
||||
}
|
||||
|
||||
for i := 0; i < peers; i++ {
|
||||
p, err := v.ExportPeer(i)
|
||||
if err != nil {
|
||||
err = core.Wrapf(err, "Peer[%v]:", i)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.Peer = append(out.Peer, p)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
type interfaceConfig struct {
|
||||
Address netip.Addr
|
||||
PrivateKey string
|
||||
ListenPort uint16
|
||||
}
|
||||
|
||||
func (p interfaceConfig) Export() (InterfaceConfig, error) {
|
||||
var err error
|
||||
|
||||
out := InterfaceConfig{
|
||||
Address: p.Address,
|
||||
ListenPort: p.ListenPort,
|
||||
}
|
||||
|
||||
out.PrivateKey, err = PrivateKeyFromBase64(p.PrivateKey)
|
||||
if err != nil {
|
||||
err = core.Wrap(err, "PrivateKey")
|
||||
return InterfaceConfig{}, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type peersConfig struct {
|
||||
PublicKey []string
|
||||
Endpoint []string
|
||||
AllowedIPs []string
|
||||
}
|
||||
|
||||
func (v *intermediateConfig) ExportPeer(i int) (PeerConfig, error) {
|
||||
var out PeerConfig
|
||||
|
||||
// Endpoint
|
||||
s := v.Peer.Endpoint[i]
|
||||
err := out.Endpoint.FromString(s)
|
||||
if err != nil {
|
||||
err = core.Wrap(err, "Endpoint")
|
||||
return out, err
|
||||
}
|
||||
|
||||
// PublicKey
|
||||
out.PublicKey, err = PublicKeyFromBase64(v.Peer.PublicKey[i])
|
||||
if err != nil {
|
||||
err = core.Wrap(err, "PublicKey")
|
||||
return out, err
|
||||
}
|
||||
|
||||
// AllowedIPs
|
||||
s = v.Peer.AllowedIPs[i]
|
||||
out.AllowedIPs, err = parseAllowedIPs(s)
|
||||
if err != nil {
|
||||
err = core.Wrap(err, "AllowedIPs")
|
||||
return out, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseAllowedIPs(data string) ([]netip.Prefix, error) {
|
||||
var out []netip.Prefix
|
||||
|
||||
@@ -235,25 +147,17 @@ func parseAllowedIPs(data string) ([]netip.Prefix, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v *intermediateConfig) PeersCount() (int, bool) {
|
||||
c0 := len(v.Peer.Endpoint)
|
||||
c1 := len(v.Peer.PublicKey)
|
||||
c2 := len(v.Peer.AllowedIPs)
|
||||
|
||||
if c0 != c1 || c1 != c2 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return c0, true
|
||||
}
|
||||
|
||||
// NewConfigFromReader parses a wgN.conf file
|
||||
func NewConfigFromReader(r io.Reader) (*Config, error) {
|
||||
temp := &intermediateConfig{}
|
||||
|
||||
if err := gcfg.ReadInto(temp, r); err != nil {
|
||||
doc, err := basic.Decode(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return temp.Export()
|
||||
cfg, err := newConfigFromDocument(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"strconv"
|
||||
|
||||
"asciigoat.org/ini/basic"
|
||||
"darvaza.org/core"
|
||||
)
|
||||
|
||||
type sectionHandler func(*Config, *basic.Section) error
|
||||
|
||||
var sectionMap = map[string]func(*Config, *basic.Section) error{
|
||||
"Interface": loadInterfaceConfSection,
|
||||
"Peer": loadPeerConfSection,
|
||||
}
|
||||
|
||||
func loadConfSection(out *Config, src *basic.Section) error {
|
||||
h, ok := sectionMap[src.Key]
|
||||
if !ok {
|
||||
return core.Wrapf(fs.ErrInvalid, "unknown section %q", src.Key)
|
||||
}
|
||||
|
||||
return h(out, src)
|
||||
}
|
||||
|
||||
func loadInterfaceConfSection(out *Config, src *basic.Section) error {
|
||||
var cfg InterfaceConfig
|
||||
|
||||
for _, field := range src.Fields {
|
||||
if err := loadInterfaceConfField(&cfg, field); err != nil {
|
||||
return core.Wrap(err, "Interface")
|
||||
}
|
||||
}
|
||||
|
||||
out.Interface = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPeerConfSection(out *Config, src *basic.Section) error {
|
||||
var cfg PeerConfig
|
||||
|
||||
for _, field := range src.Fields {
|
||||
if err := loadPeerConfField(&cfg, field); err != nil {
|
||||
return core.Wrapf(err, "Peer[%v]", len(out.Peer))
|
||||
}
|
||||
}
|
||||
|
||||
out.Peer = append(out.Peer, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// revive:disable:cyclomatic
|
||||
// revive:disable:cognitive-complexity
|
||||
|
||||
func loadInterfaceConfField(cfg *InterfaceConfig, field basic.Field) error {
|
||||
// revive:enable:cyclomatic
|
||||
// revive:enable:cognitive-complexity
|
||||
|
||||
// TODO: refactor when asciigoat's ini parser learns to do reflection
|
||||
switch field.Key {
|
||||
case "Address":
|
||||
if !core.IsZero(cfg.Address) {
|
||||
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
|
||||
}
|
||||
|
||||
err := cfg.Address.UnmarshalText([]byte(field.Value))
|
||||
switch {
|
||||
case err != nil:
|
||||
return core.Wrap(err, field.Key)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case "PrivateKey":
|
||||
if !core.IsZero(cfg.PrivateKey) {
|
||||
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
|
||||
}
|
||||
|
||||
err := cfg.PrivateKey.UnmarshalText([]byte(field.Value))
|
||||
switch {
|
||||
case err != nil:
|
||||
return core.Wrap(err, field.Key)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case "ListenPort":
|
||||
if cfg.ListenPort > 0 {
|
||||
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
|
||||
}
|
||||
|
||||
u64, err := strconv.ParseUint(field.Value, 10, 16)
|
||||
switch {
|
||||
case err != nil:
|
||||
return core.Wrap(err, field.Key)
|
||||
case u64 == 0:
|
||||
return core.Wrapf(fs.ErrInvalid, "invalid %q value", field.Key)
|
||||
default:
|
||||
cfg.ListenPort = uint16(u64)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return core.Wrapf(fs.ErrInvalid, "unknown field %q", field.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// revive:disable:cyclomatic
|
||||
// revive:disable:cognitive-complexity
|
||||
|
||||
func loadPeerConfField(cfg *PeerConfig, field basic.Field) error {
|
||||
// revive:enable:cyclomatic
|
||||
// revive:enable:cognitive-complexity
|
||||
|
||||
switch field.Key {
|
||||
case "PublicKey":
|
||||
if !core.IsZero(cfg.PublicKey) {
|
||||
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
|
||||
}
|
||||
|
||||
err := cfg.PublicKey.UnmarshalText([]byte(field.Value))
|
||||
switch {
|
||||
case err != nil:
|
||||
return core.Wrap(err, field.Key)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case "Endpoint":
|
||||
if cfg.Endpoint.String() != "" {
|
||||
return core.Wrapf(fs.ErrInvalid, "duplicate field %q", field.Key)
|
||||
}
|
||||
|
||||
err := cfg.Endpoint.UnmarshalText([]byte(field.Value))
|
||||
switch {
|
||||
case err != nil:
|
||||
return core.Wrap(err, field.Key)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
case "AllowedIPs":
|
||||
s, err := parseAllowedIPs(field.Value)
|
||||
switch {
|
||||
case err != nil:
|
||||
return core.Wrap(err, field.Key)
|
||||
case len(s) > 0:
|
||||
cfg.AllowedIPs = append(cfg.AllowedIPs, s...)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return core.Wrapf(fs.ErrInvalid, "unknown field %q", field.Key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newConfigFromDocument(doc *basic.Document) (*Config, error) {
|
||||
var out Config
|
||||
|
||||
if len(doc.Global) > 0 {
|
||||
err := core.Wrap(fs.ErrInvalid, "fields before the first section")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range doc.Sections {
|
||||
src := &doc.Sections[i]
|
||||
if err := loadConfSection(&out, src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
@@ -51,6 +51,30 @@ func (pub PublicKey) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalText loads the value from base64
|
||||
func (key *PrivateKey) UnmarshalText(b []byte) error {
|
||||
v, err := PrivateKeyFromBase64(string(b))
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
*key = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalText loads the value from base64
|
||||
func (pub *PublicKey) UnmarshalText(b []byte) error {
|
||||
v, err := PublicKeyFromBase64(string(b))
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
*pub = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the key for JSON, omitting empty.
|
||||
func (key PrivateKey) MarshalJSON() ([]byte, error) {
|
||||
return encodeKeyJSON(key.String())
|
||||
|
||||
Reference in New Issue
Block a user