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:
|
following command from their own host:
|
||||||
|
|
||||||
```
|
```
|
||||||
isle admin create-bootstrap \
|
isle hosts create \
|
||||||
--hostname <name> \
|
--hostname <name> \
|
||||||
--ip <ip> \
|
--ip <ip> \
|
||||||
--admin-path <path to admin.json> \
|
--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`
|
### Encrypted `admin.json`
|
||||||
|
|
||||||
If `admin.json` is kept in an encrypted format on disk (it should be!) then the
|
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
|
GPG is being used to secure `admin.json` then the following could be used to
|
||||||
generate a `bootstrap.json`:
|
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> \
|
--hostname <name> \
|
||||||
--ip <ip> \
|
--ip <ip> \
|
||||||
--admin-path - \
|
--admin-path - \
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"isle/admin"
|
"isle/admin"
|
||||||
"isle/bootstrap"
|
|
||||||
"isle/nebula"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
"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) {
|
func readAdmin(path string) (admin.Admin, error) {
|
||||||
|
|
||||||
if path == "-" {
|
if path == "-" {
|
||||||
@ -40,83 +26,3 @@ func readAdmin(path string) (admin.Admin, error) {
|
|||||||
|
|
||||||
return admin.FromReader(f)
|
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/daemon"
|
||||||
"isle/jsonutil"
|
"isle/jsonutil"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"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{
|
var subCmdHostsList = subCmd{
|
||||||
name: "list",
|
name: "list",
|
||||||
descr: "Lists all hosts in the network, and their IPs",
|
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",
|
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||||
do: func(subCmdCtx subCmdCtx) error {
|
do: func(subCmdCtx subCmdCtx) error {
|
||||||
return subCmdCtx.doSubCmd(
|
return subCmdCtx.doSubCmd(
|
||||||
|
subCmdHostsCreate,
|
||||||
subCmdHostsRemove,
|
subCmdHostsRemove,
|
||||||
subCmdHostsList,
|
subCmdHostsList,
|
||||||
)
|
)
|
||||||
|
@ -61,7 +61,6 @@ func main() {
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}.doSubCmd(
|
}.doSubCmd(
|
||||||
subCmdAdmin,
|
|
||||||
subCmdDaemon,
|
subCmdDaemon,
|
||||||
subCmdGarage,
|
subCmdGarage,
|
||||||
subCmdHosts,
|
subCmdHosts,
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
"isle/garage"
|
"isle/garage"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -55,6 +56,17 @@ type Daemon interface {
|
|||||||
// RemoveHost removes the host of the given name from the network.
|
// RemoveHost removes the host of the given name from the network.
|
||||||
RemoveHost(context.Context, nebula.HostName) error
|
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
|
// CreateNebulaCertificate creates and signs a new nebula certficate for an
|
||||||
// existing host, given the public key for that host. This is currently
|
// existing host, given the public key for that host. This is currently
|
||||||
// mostly useful for creating certs for mobile devices.
|
// 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(
|
func (d *daemon) CreateNebulaCertificate(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
caSigningPrivateKey nebula.SigningPrivateKey,
|
caSigningPrivateKey nebula.SigningPrivateKey,
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"isle/admin"
|
"isle/admin"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
"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)
|
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
|
// CreateNebulaCertificateRequest contains the arguments to the
|
||||||
// CreateNebulaCertificate RPC method.
|
// CreateNebulaCertificate RPC method.
|
||||||
//
|
//
|
||||||
|
@ -67,7 +67,7 @@ EOF
|
|||||||
> admin.json
|
> admin.json
|
||||||
|
|
||||||
echo "Creating secondus bootstrap"
|
echo "Creating secondus bootstrap"
|
||||||
isle admin create-bootstrap \
|
isle hosts create \
|
||||||
--admin-path admin.json \
|
--admin-path admin.json \
|
||||||
--hostname secondus \
|
--hostname secondus \
|
||||||
--ip "$secondus_ip" \
|
--ip "$secondus_ip" \
|
||||||
|
Loading…
Reference in New Issue
Block a user