isle/go/cmd/entrypoint/network.go

324 lines
7.6 KiB
Go

package main
import (
"cmp"
"errors"
"fmt"
"isle/bootstrap"
"isle/daemon"
"isle/daemon/daecommon"
"isle/daemon/network"
"isle/jsonutil"
"isle/nebula"
"isle/toolkit"
"path/filepath"
"slices"
"strconv"
"strings"
)
var subCmdNetworkCreate = subCmd{
name: "create",
descr: "Create's a new network, with this host being the first host in that network.",
do: func(ctx subCmdCtx) error {
var (
ipNet ipNetFlag
hostName hostNameFlag
vpnPublicAddr addrFlag
)
name := ctx.flags.StringP(
"name", "N", "",
"Human-readable name to identify the network as.",
)
domain := ctx.flags.StringP(
"domain", "d", "",
"Domain name that should be used as the root domain in the network.",
)
ipNetF := ctx.flags.VarPF(
&ipNet, "ip-net", "i",
`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.`,
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of this host, which will be the first host in the network",
)
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>`",
)
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
noNetwork: true,
})
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" ||
*domain == "" ||
!ipNetF.Changed ||
!hostNameF.Changed {
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}
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}
}
daemonRPC, err := ctx.newDaemonRPC()
if err != nil {
return fmt.Errorf("creating daemon RPC client: %w", err)
}
defer daemonRPC.Close()
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),
},
)
}
}),
)
}
err = daemonRPC.CreateNetwork(
ctx,
ctx.opts.bootstrapNewCreationParams(*name, *domain),
ipNet.V,
hostName.V,
&daemon.CreateNetworkOpts{Config: networkConfig},
)
if err != nil {
return fmt.Errorf("creating network: %w", err)
}
return nil
},
}
var subCmdNetworkJoin = subCmd{
name: "join",
descr: "Joins this host to an existing network",
do: func(ctx subCmdCtx) error {
bootstrapPath := ctx.flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
)
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
noNetwork: true,
})
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *bootstrapPath == "" {
return errors.New("--bootstrap-path is required")
}
var newBootstrap network.JoiningBootstrap
if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil {
return fmt.Errorf(
"loading bootstrap from %q: %w", *bootstrapPath, err,
)
}
daemonRPC, err := ctx.newDaemonRPC()
if err != nil {
return fmt.Errorf("creating daemon RPC client: %w", err)
}
defer daemonRPC.Close()
return daemonRPC.JoinNetwork(ctx, newBootstrap)
},
}
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)
},
}
var subCmdNetworkList = subCmd{
name: "list",
descr: "Lists all networks which have been joined",
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags(&withParsedFlagsOpts{
noNetwork: true,
})
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
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)
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"`
}
networkViews := make([]networkView, len(networkCreationParams))
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
}),
}
var subCmdNetworkGetConfig = subCmd{
name: "get-config",
descr: "Displays the currently active configuration for a joined network",
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags(nil)
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
daemonRPC, err := ctx.newDaemonRPC()
if err != nil {
return nil, fmt.Errorf("creating daemon RPC client: %w", err)
}
defer daemonRPC.Close()
return daemonRPC.GetConfig(ctx)
}),
}
var subCmdNetwork = subCmd{
name: "network",
descr: "Sub-commands related to network membership",
plural: "s",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdNetworkCreate,
subCmdNetworkJoin,
subCmdNetworkLeave,
subCmdNetworkList,
subCmdNetworkGetConfig,
)
},
}