2024-07-07 10:44:49 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-12-06 14:42:37 +00:00
|
|
|
"cmp"
|
2024-07-07 10:44:49 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2024-12-06 14:42:37 +00:00
|
|
|
"isle/bootstrap"
|
|
|
|
"isle/daemon"
|
2024-12-17 10:33:19 +00:00
|
|
|
"isle/daemon/daecommon"
|
2024-09-09 14:34:00 +00:00
|
|
|
"isle/daemon/network"
|
2024-07-14 09:58:39 +00:00
|
|
|
"isle/jsonutil"
|
2024-12-06 14:42:37 +00:00
|
|
|
"isle/nebula"
|
2024-12-17 10:33:19 +00:00
|
|
|
"isle/toolkit"
|
|
|
|
"path/filepath"
|
2024-12-06 14:42:37 +00:00
|
|
|
"slices"
|
2024-12-17 10:33:19 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2024-07-07 10:44:49 +00:00
|
|
|
)
|
|
|
|
|
2024-07-09 09:43:17 +00:00
|
|
|
var subCmdNetworkCreate = subCmd{
|
2024-12-10 21:07:25 +00:00
|
|
|
name: "create",
|
|
|
|
descr: "Create's a new network, with this host being the first host in that network.",
|
2024-09-04 20:35:29 +00:00
|
|
|
do: func(ctx subCmdCtx) error {
|
2024-07-09 09:43:17 +00:00
|
|
|
var (
|
2024-12-17 10:33:19 +00:00
|
|
|
ipNet ipNetFlag
|
|
|
|
hostName hostNameFlag
|
|
|
|
vpnPublicAddr addrFlag
|
2024-07-09 09:43:17 +00:00
|
|
|
)
|
|
|
|
|
2024-09-23 18:50:45 +00:00
|
|
|
name := ctx.flags.StringP(
|
2024-07-22 14:37:22 +00:00
|
|
|
"name", "N", "",
|
2024-07-09 09:43:17 +00:00
|
|
|
"Human-readable name to identify the network as.",
|
|
|
|
)
|
|
|
|
|
2024-09-23 18:50:45 +00:00
|
|
|
domain := ctx.flags.StringP(
|
2024-07-22 08:42:25 +00:00
|
|
|
"domain", "d", "",
|
2024-07-09 09:43:17 +00:00
|
|
|
"Domain name that should be used as the root domain in the network.",
|
|
|
|
)
|
|
|
|
|
2024-09-23 18:50:45 +00:00
|
|
|
ipNetF := ctx.flags.VarPF(
|
2024-07-22 08:42:25 +00:00
|
|
|
&ipNet, "ip-net", "i",
|
2024-07-12 13:30:21 +00:00
|
|
|
`An IP subnet, in CIDR form, which will be the overall range of`+
|
|
|
|
` possible IPs in the network. The first IP in this network`+
|
|
|
|
` range will become this first host's IP.`,
|
2024-07-09 09:43:17 +00:00
|
|
|
)
|
|
|
|
|
2024-09-23 18:50:45 +00:00
|
|
|
hostNameF := ctx.flags.VarPF(
|
2024-07-22 08:42:25 +00:00
|
|
|
&hostName,
|
2024-07-22 14:37:22 +00:00
|
|
|
"hostname", "n",
|
2024-07-09 09:43:17 +00:00
|
|
|
"Name of this host, which will be the first host in the network",
|
|
|
|
)
|
|
|
|
|
2024-12-17 10:33:19 +00:00
|
|
|
vpnPublicAddrF := ctx.flags.VarPF(
|
|
|
|
&vpnPublicAddr,
|
|
|
|
"vpn-public-address",
|
|
|
|
"",
|
|
|
|
"Public address (host:port) that this host is publicly available on",
|
|
|
|
)
|
|
|
|
|
|
|
|
storageAllocStrs := ctx.flags.StringArray(
|
|
|
|
"storage-allocation",
|
|
|
|
nil,
|
|
|
|
"Storage allocation on this host, in the form '<capacity-in-gb>@<path>`",
|
|
|
|
)
|
|
|
|
|
2024-12-10 21:07:25 +00:00
|
|
|
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
|
|
|
|
noNetwork: true,
|
|
|
|
})
|
2024-09-23 18:50:45 +00:00
|
|
|
if err != nil {
|
2024-07-09 09:43:17 +00:00
|
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-07-22 08:42:25 +00:00
|
|
|
if *name == "" ||
|
|
|
|
*domain == "" ||
|
2024-07-12 13:30:21 +00:00
|
|
|
!ipNetF.Changed ||
|
|
|
|
!hostNameF.Changed {
|
2024-07-09 09:43:17 +00:00
|
|
|
return errors.New("--name, --domain, --ip-net, and --hostname are required")
|
|
|
|
}
|
|
|
|
|
2024-12-17 10:33:19 +00:00
|
|
|
type storageAlloc struct {
|
|
|
|
capacity uint64
|
|
|
|
path string
|
|
|
|
}
|
|
|
|
|
|
|
|
storageAllocs := make([]storageAlloc, len(*storageAllocStrs))
|
|
|
|
for i, str := range *storageAllocStrs {
|
|
|
|
capStr, path, ok := strings.Cut(str, "@")
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"malformed --storage-allocation %q, no '@' found", str,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
capacity, err := strconv.ParseUint(capStr, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"invalid --storage-allocation capacity %q", capStr,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
storageAllocs[i] = storageAlloc{capacity, path}
|
|
|
|
}
|
|
|
|
|
2024-12-16 13:59:11 +00:00
|
|
|
daemonRPC, err := ctx.newDaemonRPC()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating daemon RPC client: %w", err)
|
|
|
|
}
|
|
|
|
defer daemonRPC.Close()
|
|
|
|
|
2024-12-17 10:33:19 +00:00
|
|
|
var networkConfig *daecommon.NetworkConfig
|
|
|
|
if vpnPublicAddrF.Changed || len(storageAllocs) > 0 {
|
|
|
|
networkConfig = toolkit.Ptr(
|
|
|
|
daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
|
|
|
|
c.VPN.PublicAddr = string(vpnPublicAddr.V)
|
|
|
|
for _, a := range storageAllocs {
|
|
|
|
c.Storage.Allocations = append(
|
|
|
|
c.Storage.Allocations,
|
|
|
|
daecommon.ConfigStorageAllocation{
|
|
|
|
DataPath: filepath.Join(a.path, "data"),
|
|
|
|
MetaPath: filepath.Join(a.path, "meta"),
|
|
|
|
Capacity: int(a.capacity),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-12-16 13:59:11 +00:00
|
|
|
err = daemonRPC.CreateNetwork(
|
2024-12-17 10:33:19 +00:00
|
|
|
ctx,
|
|
|
|
ctx.opts.bootstrapNewCreationParams(*name, *domain),
|
|
|
|
ipNet.V,
|
|
|
|
hostName.V,
|
|
|
|
&daemon.CreateNetworkOpts{Config: networkConfig},
|
2024-09-04 19:24:45 +00:00
|
|
|
)
|
2024-07-09 09:43:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating network: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-07-07 10:44:49 +00:00
|
|
|
var subCmdNetworkJoin = subCmd{
|
2024-12-10 21:07:25 +00:00
|
|
|
name: "join",
|
|
|
|
descr: "Joins this host to an existing network",
|
2024-09-04 20:35:29 +00:00
|
|
|
do: func(ctx subCmdCtx) error {
|
2024-09-23 18:50:45 +00:00
|
|
|
bootstrapPath := ctx.flags.StringP(
|
|
|
|
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
|
2024-07-07 10:44:49 +00:00
|
|
|
)
|
|
|
|
|
2024-12-10 21:07:25 +00:00
|
|
|
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
|
|
|
|
noNetwork: true,
|
|
|
|
})
|
2024-09-23 18:50:45 +00:00
|
|
|
if err != nil {
|
2024-07-07 10:44:49 +00:00
|
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if *bootstrapPath == "" {
|
|
|
|
return errors.New("--bootstrap-path is required")
|
|
|
|
}
|
|
|
|
|
2024-09-09 14:34:00 +00:00
|
|
|
var newBootstrap network.JoiningBootstrap
|
2024-07-14 09:58:39 +00:00
|
|
|
if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil {
|
2024-07-07 10:44:49 +00:00
|
|
|
return fmt.Errorf(
|
|
|
|
"loading bootstrap from %q: %w", *bootstrapPath, err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-12-16 13:59:11 +00:00
|
|
|
daemonRPC, err := ctx.newDaemonRPC()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating daemon RPC client: %w", err)
|
|
|
|
}
|
|
|
|
defer daemonRPC.Close()
|
|
|
|
|
|
|
|
return daemonRPC.JoinNetwork(ctx, newBootstrap)
|
2024-07-07 10:44:49 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-12-17 15:47:33 +00:00
|
|
|
var subCmdNetworkLeave = subCmd{
|
|
|
|
name: "leave",
|
|
|
|
descr: "Leaves a network which was previously joined or created",
|
|
|
|
do: func(ctx subCmdCtx) error {
|
|
|
|
yesImSure := ctx.flags.Bool("yes-im-sure", false, "Must be given")
|
|
|
|
|
|
|
|
ctx, err := ctx.withParsedFlags(nil)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !*yesImSure {
|
|
|
|
return errors.New("--yes-im-sure must be given")
|
|
|
|
}
|
|
|
|
|
|
|
|
daemonRPC, err := ctx.newDaemonRPC()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating daemon RPC client: %w", err)
|
|
|
|
}
|
|
|
|
defer daemonRPC.Close()
|
|
|
|
|
|
|
|
return daemonRPC.LeaveNetwork(ctx)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-09-24 09:03:18 +00:00
|
|
|
var subCmdNetworkList = subCmd{
|
2024-12-10 21:07:25 +00:00
|
|
|
name: "list",
|
|
|
|
descr: "Lists all networks which have been joined",
|
2024-11-09 16:39:49 +00:00
|
|
|
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
2024-12-10 21:07:25 +00:00
|
|
|
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
|
|
|
|
noNetwork: true,
|
|
|
|
})
|
2024-09-24 09:03:18 +00:00
|
|
|
if err != nil {
|
2024-11-09 16:39:49 +00:00
|
|
|
return nil, fmt.Errorf("parsing flags: %w", err)
|
2024-09-24 09:03:18 +00:00
|
|
|
}
|
|
|
|
|
2024-12-16 13:59:11 +00:00
|
|
|
daemonRPC, err := ctx.newDaemonRPC()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating daemon RPC client: %w", err)
|
|
|
|
}
|
|
|
|
defer daemonRPC.Close()
|
|
|
|
|
|
|
|
networkCreationParams, err := daemonRPC.GetNetworks(ctx)
|
2024-12-06 14:42:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("calling GetNetworks: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
type lighthouseView struct {
|
|
|
|
PublicAddr string `yaml:"public_addr,omitempty"`
|
|
|
|
IP string `yaml:"ip"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type networkView struct {
|
|
|
|
bootstrap.CreationParams `yaml:",inline"`
|
|
|
|
CACert nebula.Certificate `yaml:"ca_cert"`
|
|
|
|
SubnetCIDR string `yaml:"subnet_cidr"`
|
|
|
|
Lighthouses []lighthouseView `yaml:"lighthouses"`
|
|
|
|
}
|
|
|
|
|
2024-12-16 13:59:11 +00:00
|
|
|
networkViews := make([]networkView, len(networkCreationParams))
|
2024-12-06 14:42:37 +00:00
|
|
|
|
|
|
|
for i, creationParams := range networkCreationParams {
|
|
|
|
ctx := daemon.WithNetwork(ctx, creationParams.ID)
|
|
|
|
|
|
|
|
networkBootstrap, err := daemonRPC.GetBootstrap(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"calling GetBootstrap with network:%+v: %w",
|
|
|
|
networkCreationParams,
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
caCert = networkBootstrap.CAPublicCredentials.Cert
|
|
|
|
caCertDetails = caCert.Unwrap().Details
|
|
|
|
subnet = caCertDetails.Subnets[0]
|
|
|
|
|
|
|
|
lighthouseViews []lighthouseView
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, h := range networkBootstrap.HostsOrdered() {
|
|
|
|
if h.Nebula.PublicAddr == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
lighthouseViews = append(lighthouseViews, lighthouseView{
|
|
|
|
PublicAddr: h.Nebula.PublicAddr,
|
|
|
|
IP: h.IP().String(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
networkViews[i] = networkView{
|
|
|
|
CreationParams: creationParams,
|
|
|
|
CACert: caCert,
|
|
|
|
SubnetCIDR: subnet.String(),
|
|
|
|
Lighthouses: lighthouseViews,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
slices.SortFunc(networkViews, func(a, b networkView) int {
|
|
|
|
return cmp.Or(
|
|
|
|
cmp.Compare(a.Name, b.Name),
|
|
|
|
cmp.Compare(a.ID, b.ID),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return networkViews, nil
|
2024-11-09 16:39:49 +00:00
|
|
|
}),
|
2024-09-24 09:03:18 +00:00
|
|
|
}
|
|
|
|
|
2024-11-12 17:18:47 +00:00
|
|
|
var subCmdNetworkGetConfig = subCmd{
|
|
|
|
name: "get-config",
|
|
|
|
descr: "Displays the currently active configuration for a joined network",
|
|
|
|
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
|
2024-12-10 21:07:25 +00:00
|
|
|
ctx, err := ctx.withParsedFlags(nil)
|
2024-11-12 17:18:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("parsing flags: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-12-16 13:59:11 +00:00
|
|
|
daemonRPC, err := ctx.newDaemonRPC()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("creating daemon RPC client: %w", err)
|
|
|
|
}
|
|
|
|
defer daemonRPC.Close()
|
|
|
|
|
|
|
|
return daemonRPC.GetConfig(ctx)
|
2024-11-12 17:18:47 +00:00
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2024-07-07 10:44:49 +00:00
|
|
|
var subCmdNetwork = subCmd{
|
2024-09-24 09:03:18 +00:00
|
|
|
name: "network",
|
|
|
|
descr: "Sub-commands related to network membership",
|
|
|
|
plural: "s",
|
2024-09-04 20:35:29 +00:00
|
|
|
do: func(ctx subCmdCtx) error {
|
|
|
|
return ctx.doSubCmd(
|
2024-07-09 09:43:17 +00:00
|
|
|
subCmdNetworkCreate,
|
2024-07-07 10:44:49 +00:00
|
|
|
subCmdNetworkJoin,
|
2024-12-17 15:47:33 +00:00
|
|
|
subCmdNetworkLeave,
|
2024-09-24 09:03:18 +00:00
|
|
|
subCmdNetworkList,
|
2024-11-12 17:18:47 +00:00
|
|
|
subCmdNetworkGetConfig,
|
2024-07-07 10:44:49 +00:00
|
|
|
)
|
|
|
|
},
|
|
|
|
}
|