3d6ed8604a
The new commands are: - `isle admin create-nebula-cert` - `isle nebula show` Between these two commands it's possible, with some effort, to get a nebula mobile client hooked up to an isle server.
429 lines
12 KiB
Go
429 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"isle/admin"
|
|
"isle/bootstrap"
|
|
"isle/daemon"
|
|
"isle/garage"
|
|
"isle/nebula"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
|
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
|
)
|
|
|
|
func randStr(l int) string {
|
|
b := make([]byte, l)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func readAdmin(path string) (admin.Admin, error) {
|
|
|
|
if path == "-" {
|
|
|
|
adm, err := admin.FromReader(os.Stdin)
|
|
if err != nil {
|
|
return admin.Admin{}, fmt.Errorf("parsing admin.yml from stdin: %w", err)
|
|
}
|
|
|
|
return adm, nil
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
return admin.FromReader(f)
|
|
}
|
|
|
|
var subCmdAdminCreateNetwork = subCmd{
|
|
name: "create-network",
|
|
descr: "Creates a new isle network, outputting the resulting admin.yml to stdout",
|
|
do: func(subCmdCtx subCmdCtx) error {
|
|
|
|
flags := subCmdCtx.flagSet(false)
|
|
|
|
daemonConfigPath := flags.StringP(
|
|
"config-path", "c", "",
|
|
"Optional path to a daemon.yml file to load configuration from.",
|
|
)
|
|
|
|
dumpConfig := flags.Bool(
|
|
"dump-config", false,
|
|
"Write the default configuration file to stdout and exit.",
|
|
)
|
|
|
|
name := flags.StringP(
|
|
"name", "n", "",
|
|
"Human-readable name to identify the network as.",
|
|
)
|
|
|
|
domain := flags.StringP(
|
|
"domain", "d", "",
|
|
"Domain name that should be used as the root domain in the network.",
|
|
)
|
|
|
|
ipNetStr := flags.StringP(
|
|
"ip-net", "i", "",
|
|
`IP+prefix (e.g. "10.10.0.1/16") which denotes the IP of this host, which will be the first host in the network, and the range of IPs which other hosts in the network can be assigned`,
|
|
)
|
|
|
|
hostName := flags.StringP(
|
|
"hostname", "h", "",
|
|
"Name of this host, which will be the first host in the network",
|
|
)
|
|
|
|
logLevelStr := flags.StringP(
|
|
"log-level", "l", "info",
|
|
`Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes`,
|
|
)
|
|
|
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
}
|
|
|
|
ctx := subCmdCtx.ctx
|
|
|
|
if *dumpConfig {
|
|
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
|
|
}
|
|
|
|
if *name == "" || *domain == "" || *ipNetStr == "" || *hostName == "" {
|
|
return errors.New("--name, --domain, --ip-net, and --hostname are required")
|
|
}
|
|
|
|
logLevel := mlog.LevelFromString(*logLevelStr)
|
|
if logLevel == nil {
|
|
return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
|
|
}
|
|
|
|
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int())
|
|
|
|
*domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".")
|
|
|
|
ip, subnet, err := net.ParseCIDR(*ipNetStr)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing %q as a CIDR: %w", *ipNetStr, err)
|
|
}
|
|
|
|
if err := validateHostName(*hostName); err != nil {
|
|
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
|
|
}
|
|
|
|
runtimeDirCleanup, err := setupAndLockRuntimeDir(ctx, logger)
|
|
if err != nil {
|
|
return fmt.Errorf("setting up runtime directory: %w", err)
|
|
}
|
|
defer runtimeDirCleanup()
|
|
|
|
daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("loading daemon config: %w", err)
|
|
}
|
|
|
|
if len(daemonConfig.Storage.Allocations) < 3 {
|
|
return fmt.Errorf("daemon config with at least 3 allocations was not provided")
|
|
}
|
|
|
|
nebulaCACreds, err := nebula.NewCACredentials(*domain, subnet)
|
|
if err != nil {
|
|
return fmt.Errorf("creating nebula CA cert: %w", err)
|
|
}
|
|
|
|
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *hostName, ip)
|
|
if err != nil {
|
|
return fmt.Errorf("creating nebula cert for host: %w", err)
|
|
}
|
|
|
|
nebulaHostSignedPublicCreds, err := bootstrap.NewNebulaHostSignedPublicCredentials(
|
|
nebulaCACreds,
|
|
nebulaHostCreds.Public,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("creating signed public credentials for host: %w", err)
|
|
}
|
|
|
|
adminCreationParams := admin.CreationParams{
|
|
ID: randStr(32),
|
|
Name: *name,
|
|
Domain: *domain,
|
|
}
|
|
|
|
hostBootstrap := bootstrap.Bootstrap{
|
|
AdminCreationParams: adminCreationParams,
|
|
Hosts: map[string]bootstrap.Host{
|
|
*hostName: bootstrap.Host{
|
|
Name: *hostName,
|
|
Nebula: bootstrap.NebulaHost{
|
|
SignedPublicCredentials: nebulaHostSignedPublicCreds,
|
|
},
|
|
},
|
|
},
|
|
HostName: *hostName,
|
|
}
|
|
|
|
hostBootstrap.Nebula.CAPublicCredentials = nebulaCACreds.Public
|
|
hostBootstrap.Nebula.HostCredentials = nebulaHostCreds
|
|
hostBootstrap.Nebula.SignedPublicCredentials = nebulaHostSignedPublicCreds
|
|
|
|
hostBootstrap.Garage.RPCSecret = randStr(32)
|
|
hostBootstrap.Garage.AdminToken = randStr(32)
|
|
hostBootstrap.Garage.GlobalBucketS3APICredentials = garage.NewS3APICredentials()
|
|
|
|
if hostBootstrap, daemonConfig, err = coalesceDaemonConfigAndBootstrap(hostBootstrap, daemonConfig); err != nil {
|
|
return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
|
|
}
|
|
|
|
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("generating nebula config: %w", err)
|
|
}
|
|
|
|
garagePmuxProcConfigs, err := garagePmuxProcConfigs(hostBootstrap, daemonConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("generating garage configs: %w", err)
|
|
}
|
|
|
|
pmuxConfig := pmuxlib.Config{
|
|
Processes: append(
|
|
[]pmuxlib.ProcessConfig{
|
|
nebulaPmuxProcConfig,
|
|
},
|
|
garagePmuxProcConfigs...,
|
|
),
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
pmuxDoneCh := make(chan struct{})
|
|
|
|
logger.Info(ctx, "starting child processes")
|
|
go func() {
|
|
// NOTE both stdout and stderr are sent to stderr, so that the user
|
|
// can pipe the resulting admin.yml to stdout.
|
|
pmuxlib.Run(ctx, os.Stderr, os.Stderr, pmuxConfig)
|
|
close(pmuxDoneCh)
|
|
}()
|
|
|
|
defer func() {
|
|
cancel()
|
|
logger.Info(ctx, "waiting for child processes to exit")
|
|
<-pmuxDoneCh
|
|
}()
|
|
|
|
logger.Info(ctx, "waiting for garage instances to come online")
|
|
if err := waitForGarageAndNebula(ctx, logger, hostBootstrap, daemonConfig); err != nil {
|
|
return fmt.Errorf("waiting for garage to start up: %w", err)
|
|
}
|
|
|
|
logger.Info(ctx, "applying initial garage layout")
|
|
if err := garageApplyLayout(ctx, logger, hostBootstrap, daemonConfig); err != nil {
|
|
return fmt.Errorf("applying initial garage layout: %w", err)
|
|
}
|
|
|
|
logger.Info(ctx, "initializing garage shared global bucket")
|
|
err = garageInitializeGlobalBucket(ctx, logger, hostBootstrap, daemonConfig)
|
|
|
|
if cErr := (garage.AdminClientError{}); errors.As(err, &cErr) && cErr.StatusCode == 409 {
|
|
return fmt.Errorf("shared global bucket has already been created, are the storage allocations from a previously initialized isle being used?")
|
|
|
|
} else if err != nil {
|
|
return fmt.Errorf("initializing garage shared global bucket: %w", err)
|
|
}
|
|
|
|
logger.Info(ctx, "cluster initialized successfully, writing admin.yml to stdout")
|
|
|
|
adm := admin.Admin{
|
|
CreationParams: adminCreationParams,
|
|
}
|
|
adm.Nebula.CACredentials = nebulaCACreds
|
|
adm.Garage.RPCSecret = hostBootstrap.Garage.RPCSecret
|
|
adm.Garage.GlobalBucketS3APICredentials = hostBootstrap.Garage.GlobalBucketS3APICredentials
|
|
|
|
if err := adm.WriteTo(os.Stdout); err != nil {
|
|
return fmt.Errorf("writing admin.yml to stdout")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var subCmdAdminCreateBootstrap = subCmd{
|
|
name: "create-bootstrap",
|
|
descr: "Creates a new bootstrap.yml file for a particular host and writes it to stdout",
|
|
checkLock: true,
|
|
do: func(subCmdCtx subCmdCtx) error {
|
|
|
|
flags := subCmdCtx.flagSet(false)
|
|
|
|
hostName := flags.StringP(
|
|
"hostname", "h", "",
|
|
"Name of the host to generate bootstrap.yml for",
|
|
)
|
|
|
|
ipStr := flags.StringP(
|
|
"ip", "i", "",
|
|
"IP of the new host",
|
|
)
|
|
|
|
adminPath := flags.StringP(
|
|
"admin-path", "a", "",
|
|
`Path to admin.yml file. If the given path is "-" then stdin is used.`,
|
|
)
|
|
|
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
}
|
|
|
|
if *hostName == "" || *ipStr == "" || *adminPath == "" {
|
|
return errors.New("--hostname, --ip, and --admin-path are required")
|
|
}
|
|
|
|
if err := validateHostName(*hostName); err != nil {
|
|
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
|
|
}
|
|
|
|
ip := net.ParseIP(*ipStr)
|
|
|
|
if ip == nil {
|
|
return fmt.Errorf("invalid ip %q", *ipStr)
|
|
}
|
|
|
|
adm, err := readAdmin(*adminPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading admin.yml with --admin-path of %q: %w", *adminPath, err)
|
|
}
|
|
|
|
hostBootstrap, err := loadHostBootstrap()
|
|
if err != nil {
|
|
return fmt.Errorf("loading host bootstrap: %w", err)
|
|
}
|
|
|
|
nebulaHostCreds, err := nebula.NewHostCredentials(adm.Nebula.CACredentials, *hostName, ip)
|
|
if err != nil {
|
|
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
|
}
|
|
|
|
nebulaHostSignedPublicCreds, err := bootstrap.NewNebulaHostSignedPublicCredentials(
|
|
adm.Nebula.CACredentials,
|
|
nebulaHostCreds.Public,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("creating signed public credentials for host: %w", err)
|
|
}
|
|
|
|
newHostBootstrap := bootstrap.Bootstrap{
|
|
AdminCreationParams: adm.CreationParams,
|
|
|
|
Hosts: hostBootstrap.Hosts,
|
|
HostName: *hostName,
|
|
}
|
|
|
|
newHostBootstrap.Nebula.CAPublicCredentials = adm.Nebula.CACredentials.Public
|
|
newHostBootstrap.Nebula.HostCredentials = nebulaHostCreds
|
|
newHostBootstrap.Nebula.SignedPublicCredentials = nebulaHostSignedPublicCreds
|
|
|
|
newHostBootstrap.Garage.RPCSecret = adm.Garage.RPCSecret
|
|
newHostBootstrap.Garage.AdminToken = randStr(32)
|
|
newHostBootstrap.Garage.GlobalBucketS3APICredentials = adm.Garage.GlobalBucketS3APICredentials
|
|
|
|
return newHostBootstrap.WriteTo(os.Stdout)
|
|
},
|
|
}
|
|
|
|
var subCmdAdminCreateNebulaCert = subCmd{
|
|
name: "create-nebula-cert",
|
|
descr: "Creates a signed nebula certificate file and writes it to stdout",
|
|
checkLock: false,
|
|
do: func(subCmdCtx subCmdCtx) error {
|
|
|
|
flags := subCmdCtx.flagSet(false)
|
|
|
|
hostName := flags.StringP(
|
|
"hostname", "h", "",
|
|
"Name of the host to generate bootstrap.yml for",
|
|
)
|
|
|
|
ipStr := flags.StringP(
|
|
"ip", "i", "",
|
|
"IP of the new host",
|
|
)
|
|
|
|
adminPath := flags.StringP(
|
|
"admin-path", "a", "",
|
|
`Path to admin.yml file. If the given path is "-" then stdin is used.`,
|
|
)
|
|
|
|
pubKeyPath := flags.StringP(
|
|
"public-key-path", "p", "",
|
|
`Path to PEM file containing public key which will be embedded in the cert.`,
|
|
)
|
|
|
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
}
|
|
|
|
if *hostName == "" || *ipStr == "" || *adminPath == "" || *pubKeyPath == "" {
|
|
return errors.New("--hostname, --ip, --admin-path, and --pub-key-path are required")
|
|
}
|
|
|
|
if err := validateHostName(*hostName); err != nil {
|
|
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
|
|
}
|
|
|
|
ip := net.ParseIP(*ipStr)
|
|
|
|
if ip == nil {
|
|
return fmt.Errorf("invalid ip %q", *ipStr)
|
|
}
|
|
|
|
adm, err := readAdmin(*adminPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading admin.yml with --admin-path of %q: %w", *adminPath, err)
|
|
}
|
|
|
|
hostPubPEM, err := os.ReadFile(*pubKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
|
|
}
|
|
|
|
nebulaHostCertPEM, err := nebula.NewHostCertPEM(
|
|
adm.Nebula.CACredentials, string(hostPubPEM), *hostName, ip,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("creating cert: %w", err)
|
|
}
|
|
|
|
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
|
|
return fmt.Errorf("writing to stdout: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var subCmdAdmin = subCmd{
|
|
name: "admin",
|
|
descr: "Sub-commands which only admins can run",
|
|
do: func(subCmdCtx subCmdCtx) error {
|
|
return subCmdCtx.doSubCmd(
|
|
subCmdAdminCreateNetwork,
|
|
subCmdAdminCreateBootstrap,
|
|
subCmdAdminCreateNebulaCert,
|
|
)
|
|
},
|
|
}
|