Move create-bootstrap logic into daemon, rename to hosts create
This commit is contained in:
parent
cb8fef38c4
commit
b5059be7fa
@ -29,7 +29,7 @@ To create a `bootstrap.json` file for the new host, the admin should perform the
|
||||
following command from their own host:
|
||||
|
||||
```
|
||||
isle admin create-bootstrap \
|
||||
isle hosts create \
|
||||
--hostname <name> \
|
||||
--ip <ip> \
|
||||
--admin-path <path to admin.json> \
|
||||
@ -48,12 +48,12 @@ The user can now proceed with calling `isle network join`, as described in the
|
||||
### Encrypted `admin.json`
|
||||
|
||||
If `admin.json` is kept in an encrypted format on disk (it should be!) then the
|
||||
decrypted form can be piped into `create-bootstrap` over stdin. For example, if
|
||||
decrypted form can be piped into `isle hosts create` over stdin. For example, if
|
||||
GPG is being used to secure `admin.json` then the following could be used to
|
||||
generate a `bootstrap.json`:
|
||||
|
||||
```
|
||||
gpg -d <path to admin.json.gpg> | isle admin create-bootstrap \
|
||||
gpg -d <path to admin.json.gpg> | isle hosts create \
|
||||
--hostname <name> \
|
||||
--ip <ip> \
|
||||
--admin-path - \
|
||||
|
@ -1,25 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"isle/admin"
|
||||
"isle/bootstrap"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
)
|
||||
|
||||
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 == "-" {
|
||||
@ -40,83 +26,3 @@ func readAdmin(path string) (admin.Admin, error) {
|
||||
|
||||
return admin.FromReader(f)
|
||||
}
|
||||
|
||||
var subCmdAdminCreateBootstrap = subCmd{
|
||||
name: "create-bootstrap",
|
||||
descr: "Creates a new bootstrap.json file for a particular host and writes it to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
ip netip.Addr
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
"Name of the host to generate bootstrap.json for",
|
||||
)
|
||||
|
||||
ipF := flags.VarPF(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.json 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 !hostNameF.Changed ||
|
||||
!ipF.Changed ||
|
||||
*adminPath == "" {
|
||||
return errors.New("--hostname, --ip, and --admin-path are required")
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
garageBootstrap := bootstrap.Garage{
|
||||
RPCSecret: adm.Garage.RPCSecret,
|
||||
AdminToken: randStr(32),
|
||||
GlobalBucketS3APICredentials: adm.Garage.GlobalBucketS3APICredentials,
|
||||
}
|
||||
|
||||
newHostBootstrap, err := bootstrap.New(
|
||||
adm.Nebula.CACredentials,
|
||||
adm.CreationParams,
|
||||
garageBootstrap,
|
||||
hostName,
|
||||
ip,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing bootstrap data: %w", err)
|
||||
}
|
||||
|
||||
hostsRes, err := subCmdCtx.getHosts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting hosts: %w", err)
|
||||
}
|
||||
|
||||
for _, host := range hostsRes.Hosts {
|
||||
newHostBootstrap.Hosts[host.Name] = host
|
||||
}
|
||||
|
||||
return newHostBootstrap.WriteTo(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdAdmin = subCmd{
|
||||
name: "admin",
|
||||
descr: "Sub-commands which only admins can run",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdAdminCreateBootstrap,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -7,10 +7,70 @@ import (
|
||||
"isle/daemon"
|
||||
"isle/jsonutil"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var subCmdHostsCreate = subCmd{
|
||||
name: "create",
|
||||
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
var (
|
||||
flags = subCmdCtx.flagSet(false)
|
||||
hostName nebula.HostName
|
||||
ip netip.Addr
|
||||
)
|
||||
|
||||
hostNameF := flags.VarPF(
|
||||
textUnmarshalerFlag{&hostName},
|
||||
"hostname", "h",
|
||||
"Name of the host to generate bootstrap.json for",
|
||||
)
|
||||
|
||||
ipF := flags.VarPF(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.json 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 !hostNameF.Changed ||
|
||||
!ipF.Changed ||
|
||||
*adminPath == "" {
|
||||
return errors.New("--hostname, --ip, and --admin-path are required")
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
var res daemon.CreateHostResult
|
||||
err = subCmdCtx.daemonRCPClient.Call(
|
||||
subCmdCtx.ctx,
|
||||
&res,
|
||||
"CreateHost",
|
||||
daemon.CreateHostRequest{
|
||||
CASigningPrivateKey: adm.Nebula.CACredentials.SigningPrivateKey,
|
||||
HostName: hostName,
|
||||
IP: ip,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling CreateHost: %w", err)
|
||||
}
|
||||
|
||||
return res.HostBootstrap.WriteTo(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdHostsList = subCmd{
|
||||
name: "list",
|
||||
descr: "Lists all hosts in the network, and their IPs",
|
||||
@ -88,6 +148,7 @@ var subCmdHosts = subCmd{
|
||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdHostsCreate,
|
||||
subCmdHostsRemove,
|
||||
subCmdHostsList,
|
||||
)
|
||||
|
@ -61,7 +61,6 @@ func main() {
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}.doSubCmd(
|
||||
subCmdAdmin,
|
||||
subCmdDaemon,
|
||||
subCmdGarage,
|
||||
subCmdHosts,
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"isle/bootstrap"
|
||||
"isle/garage"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@ -55,6 +56,17 @@ type Daemon interface {
|
||||
// RemoveHost removes the host of the given name from the network.
|
||||
RemoveHost(context.Context, nebula.HostName) error
|
||||
|
||||
// CreateHost creates a bootstrap for a new host with the given name and IP
|
||||
// address.
|
||||
CreateHost(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey, // TODO load from secrets storage
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr, // TODO automatically choose IP address
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
)
|
||||
|
||||
// CreateNebulaCertificate creates and signs a new nebula certficate for an
|
||||
// existing host, given the public key for that host. This is currently
|
||||
// mostly useful for creating certs for mobile devices.
|
||||
@ -617,6 +629,54 @@ func makeCACreds(
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) CreateHost(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey,
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
return withCurrBootstrap(d, func(
|
||||
currBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
var (
|
||||
garageRPCSecret = currBootstrap.Garage.RPCSecret
|
||||
garageGlobalBucketS3APICreds = currBootstrap.Garage.GlobalBucketS3APICredentials
|
||||
)
|
||||
|
||||
// TODO check if garageRPCSecret is actually set
|
||||
|
||||
garageBootstrap := bootstrap.Garage{
|
||||
RPCSecret: garageRPCSecret,
|
||||
AdminToken: randStr(32),
|
||||
GlobalBucketS3APICredentials: garageGlobalBucketS3APICreds,
|
||||
}
|
||||
|
||||
newHostBootstrap, err := bootstrap.New(
|
||||
makeCACreds(currBootstrap, caSigningPrivateKey),
|
||||
currBootstrap.AdminCreationParams,
|
||||
garageBootstrap,
|
||||
hostName,
|
||||
ip,
|
||||
)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf(
|
||||
"initializing bootstrap data: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
newHostBootstrap.Hosts = currBootstrap.Hosts
|
||||
|
||||
// TODO persist new bootstrap to garage. Requires making the daemon
|
||||
// config change watching logic smarter, so only dnsmasq gets restarted.
|
||||
|
||||
return newHostBootstrap, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (d *daemon) CreateNebulaCertificate(
|
||||
ctx context.Context,
|
||||
caSigningPrivateKey nebula.SigningPrivateKey,
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"isle/admin"
|
||||
"isle/bootstrap"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
@ -131,6 +132,38 @@ func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{},
|
||||
return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName)
|
||||
}
|
||||
|
||||
// CreateHostRequest contains the arguments to the
|
||||
// CreateHost RPC method.
|
||||
//
|
||||
// All fields are required.
|
||||
type CreateHostRequest struct {
|
||||
CASigningPrivateKey nebula.SigningPrivateKey // TODO load from secrets storage
|
||||
HostName nebula.HostName
|
||||
IP netip.Addr
|
||||
}
|
||||
|
||||
// CreateHostResult wraps the results from the CreateHost RPC method.
|
||||
type CreateHostResult struct {
|
||||
HostBootstrap bootstrap.Bootstrap
|
||||
}
|
||||
|
||||
// CreateHost passes the call through to the Daemon method of the
|
||||
// same name.
|
||||
func (r *RPC) CreateHost(
|
||||
ctx context.Context, req CreateHostRequest,
|
||||
) (
|
||||
CreateHostResult, error,
|
||||
) {
|
||||
hostBootstrap, err := r.daemon.CreateHost(
|
||||
ctx, req.CASigningPrivateKey, req.HostName, req.IP,
|
||||
)
|
||||
if err != nil {
|
||||
return CreateHostResult{}, err
|
||||
}
|
||||
|
||||
return CreateHostResult{HostBootstrap: hostBootstrap}, nil
|
||||
}
|
||||
|
||||
// CreateNebulaCertificateRequest contains the arguments to the
|
||||
// CreateNebulaCertificate RPC method.
|
||||
//
|
||||
|
@ -67,7 +67,7 @@ EOF
|
||||
> admin.json
|
||||
|
||||
echo "Creating secondus bootstrap"
|
||||
isle admin create-bootstrap \
|
||||
isle hosts create \
|
||||
--admin-path admin.json \
|
||||
--hostname secondus \
|
||||
--ip "$secondus_ip" \
|
||||
|
Loading…
Reference in New Issue
Block a user