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 '@`", ) 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, ) }, }