Files
ionscale/internal/cmd/machine.go
T
2025-02-15 15:17:37 +01:00

523 lines
13 KiB
Go

package cmd
import (
"fmt"
"github.com/bufbuild/connect-go"
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
"github.com/nleeper/goment"
"github.com/rodaine/table"
"github.com/spf13/cobra"
"inet.af/netaddr"
"os"
"strings"
"text/tabwriter"
)
func machineCommands() *cobra.Command {
command := &cobra.Command{
Use: "machines",
Aliases: []string{"machine", "devices", "device"},
Short: "Manage ionscale machines",
SilenceUsage: true,
}
command.AddCommand(getMachineCommand())
command.AddCommand(deleteMachineCommand())
command.AddCommand(expireMachineCommand())
command.AddCommand(listMachinesCommand())
command.AddCommand(getMachineRoutesCommand())
command.AddCommand(enableMachineRoutesCommand())
command.AddCommand(disableMachineRoutesCommand())
command.AddCommand(enableMachineKeyExpiryCommand())
command.AddCommand(enableExitNodeCommand())
command.AddCommand(disableExitNodeCommand())
command.AddCommand(disableMachineKeyExpiryCommand())
command.AddCommand(authorizeMachineCommand())
command.AddCommand(setMachineNameCommand())
return command
}
func getMachineCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "get",
Short: "Retrieve detailed information for a machine",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.GetMachineRequest{MachineId: machineID}
resp, err := tc.Client().GetMachine(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
m := resp.Msg.Machine
var lastSeen = "N/A"
var expiresAt = "No expiry"
if m.LastSeen != nil && !m.LastSeen.AsTime().IsZero() {
if mom, err := goment.New(m.LastSeen.AsTime()); err == nil {
lastSeen = mom.FromNow()
}
}
if !m.KeyExpiryDisabled && m.ExpiresAt != nil && !m.ExpiresAt.AsTime().IsZero() {
if mom, err := goment.New(m.ExpiresAt.AsTime()); !m.ExpiresAt.AsTime().IsZero() && err == nil {
expiresAt = mom.FromNow()
}
}
// initialize tabwriter
w := new(tabwriter.Writer)
// minwidth, tabwidth, padding, padchar, flags
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
defer w.Flush()
fmt.Fprintf(w, "%s\t%d\n", "ID", m.Id)
fmt.Fprintf(w, "%s\t%s\n", "Machine name", m.Name)
fmt.Fprintf(w, "%s\t%s\n", "Creator", m.User.Name)
fmt.Fprintf(w, "%s\t%s\n", "OS", m.Os)
fmt.Fprintf(w, "%s\t%s\n", "Tailscale version", m.ClientVersion)
fmt.Fprintf(w, "%s\t%s\n", "Tailscale IPv4", m.Ipv4)
fmt.Fprintf(w, "%s\t%s\n", "Tailscale IPv6", m.Ipv6)
fmt.Fprintf(w, "%s\t%s\n", "Last seen", lastSeen)
fmt.Fprintf(w, "%s\t%v\n", "Ephemeral", m.Ephemeral)
if !m.Authorized {
fmt.Fprintf(w, "%s\t%v\n", "Authorized", m.Authorized)
}
fmt.Fprintf(w, "%s\t%s\n", "Key expiry", expiresAt)
for i, t := range m.Tags {
if i == 0 {
fmt.Fprintf(w, "%s\t%s\n", "ACL tags", t)
} else {
fmt.Fprintf(w, "%s\t%s\n", "", t)
}
}
for i, e := range m.ClientConnectivity.Endpoints {
if i == 0 {
fmt.Fprintf(w, "%s\t%s\n", "Endpoints", e)
} else {
fmt.Fprintf(w, "%s\t%s\n", "", e)
}
}
for i, t := range m.AdvertisedRoutes {
if i == 0 {
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
} else {
fmt.Fprintf(w, "%s\t%s\n", "", t)
}
}
for i, t := range m.EnabledRoutes {
if i == 0 {
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
} else {
fmt.Fprintf(w, "%s\t%s\n", "", t)
}
}
if m.AdvertisedExitNode {
if m.EnabledExitNode {
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "enabled")
} else {
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "disabled")
}
} else {
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "no")
}
return nil
}
return command
}
func deleteMachineCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "delete",
Short: "Deletes a machine",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.DeleteMachineRequest{MachineId: machineID}
if _, err := tc.Client().DeleteMachine(cmd.Context(), connect.NewRequest(&req)); err != nil {
return err
}
fmt.Println("Machine deleted.")
return nil
}
return command
}
func setMachineNameCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "set-name",
Short: "Set the name of a given machine",
SilenceUsage: true,
})
var machineID uint64
var useOSHostname bool
var name string
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
command.Flags().StringVar(&name, "name", "", "New name for the machine")
command.Flags().BoolVar(&useOSHostname, "use-os-hostname", false, "Auto-generate from the machine OS hostname")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
if !useOSHostname && name == "" {
return fmt.Errorf("name is required when not using os hostname")
}
req := api.SetMachineNameRequest{MachineId: machineID, Name: name, UseOsHostname: useOSHostname}
if _, err := tc.Client().SetMachineName(cmd.Context(), connect.NewRequest(&req)); err != nil {
return err
}
fmt.Println("Machine name set.")
return nil
}
return command
}
func expireMachineCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "expire",
Short: "Expires a machine",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.ExpireMachineRequest{MachineId: machineID}
if _, err := tc.Client().ExpireMachine(cmd.Context(), connect.NewRequest(&req)); err != nil {
return err
}
fmt.Println("Machine key expired.")
return nil
}
return command
}
func authorizeMachineCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "authorize",
Short: "Authorizes a machine",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.AuthorizeMachineRequest{MachineId: machineID}
if _, err := tc.Client().AuthorizeMachine(cmd.Context(), connect.NewRequest(&req)); err != nil {
return err
}
fmt.Println("Machine authorized.")
return nil
}
return command
}
func listMachinesCommand() *cobra.Command {
command, tc := prepareCommand(true, &cobra.Command{
Use: "list",
Short: "List machines",
SilenceUsage: true,
})
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.ListMachinesRequest{TailnetId: tc.TailnetID()}
resp, err := tc.Client().ListMachines(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
tbl := table.New("ID", "TAILNET", "NAME", "IPv4", "IPv6", "AUTHORIZED", "EPHEMERAL", "VERSION", "LAST_SEEN", "TAGS")
for _, m := range resp.Msg.Machines {
var lastSeen = "N/A"
if m.Connected {
lastSeen = "Connected"
} else if m.LastSeen != nil {
mom, err := goment.New(m.LastSeen.AsTime())
if err == nil {
lastSeen = mom.FromNow()
}
}
tbl.AddRow(m.Id, m.Tailnet.Name, m.Name, m.Ipv4, m.Ipv6, m.Authorized, m.Ephemeral, m.ClientVersion, lastSeen, strings.Join(m.Tags, ","))
}
tbl.Print()
return nil
}
return command
}
func getMachineRoutesCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "get-routes",
Short: "Show routes advertised and enabled by a given machine",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID.")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.GetMachineRoutesRequest{MachineId: machineID}
resp, err := tc.Client().GetMachineRoutes(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
printMachinesRoutesResponse(resp.Msg.Routes)
return nil
}
return command
}
func enableMachineRoutesCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "enable-routes",
Short: "Enable routes for a given machine",
SilenceUsage: true,
})
var machineID uint64
var routes []string
var replace bool
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
command.Flags().StringSliceVar(&routes, "routes", []string{}, "List of routes to enable")
command.Flags().BoolVar(&replace, "replace", false, "Replace current enabled routes with this new list")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
for _, r := range routes {
if _, err := netaddr.ParseIPPrefix(r); err != nil {
return err
}
}
req := api.EnableMachineRoutesRequest{MachineId: machineID, Routes: routes, Replace: replace}
resp, err := tc.Client().EnableMachineRoutes(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
printMachinesRoutesResponse(resp.Msg.Routes)
return nil
}
return command
}
func disableMachineRoutesCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "disable-routes",
Short: "Disable routes for a given machine",
SilenceUsage: true,
})
var machineID uint64
var routes []string
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
command.Flags().StringSliceVar(&routes, "routes", []string{}, "List of routes to enable")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
for _, r := range routes {
if _, err := netaddr.ParseIPPrefix(r); err != nil {
return err
}
}
req := api.DisableMachineRoutesRequest{MachineId: machineID, Routes: routes}
resp, err := tc.Client().DisableMachineRoutes(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
printMachinesRoutesResponse(resp.Msg.Routes)
return nil
}
return command
}
func enableExitNodeCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "enable-exit-node",
Short: "Enable given machine as an exit node",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.EnableExitNodeRequest{MachineId: machineID}
resp, err := tc.Client().EnableExitNode(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
printMachinesRoutesResponse(resp.Msg.Routes)
return nil
}
return command
}
func disableExitNodeCommand() *cobra.Command {
command, tc := prepareCommand(false, &cobra.Command{
Use: "disable-exit-node",
Short: "Disable given machine as an exit node",
SilenceUsage: true,
})
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.DisableExitNodeRequest{MachineId: machineID}
resp, err := tc.Client().DisableExitNode(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
printMachinesRoutesResponse(resp.Msg.Routes)
return nil
}
return command
}
func enableMachineKeyExpiryCommand() *cobra.Command {
command := &cobra.Command{
Use: "enable-key-expiry",
Short: "Enable machine key expiry",
SilenceUsage: true,
}
return configureSetMachineKeyExpiryCommand(command, false)
}
func disableMachineKeyExpiryCommand() *cobra.Command {
command := &cobra.Command{
Use: "disable-key-expiry",
Short: "Disable machine key expiry",
SilenceUsage: true,
}
return configureSetMachineKeyExpiryCommand(command, true)
}
func configureSetMachineKeyExpiryCommand(cmdTmpl *cobra.Command, disable bool) *cobra.Command {
command, tc := prepareCommand(false, cmdTmpl)
var machineID uint64
command.Flags().Uint64Var(&machineID, "machine-id", 0, "Machine ID")
_ = command.MarkFlagRequired("machine-id")
command.RunE = func(cmd *cobra.Command, args []string) error {
req := api.SetMachineKeyExpiryRequest{MachineId: machineID, Disabled: disable}
_, err := tc.Client().SetMachineKeyExpiry(cmd.Context(), connect.NewRequest(&req))
if err != nil {
return err
}
return nil
}
return command
}
func printMachinesRoutesResponse(msg *api.MachineRoutes) {
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
defer w.Flush()
for i, t := range msg.AdvertisedRoutes {
if i == 0 {
fmt.Fprintf(w, "%s\t%s\n", "Advertised routes", t)
} else {
fmt.Fprintf(w, "%s\t%s\n", "", t)
}
}
for i, t := range msg.EnabledRoutes {
if i == 0 {
fmt.Fprintf(w, "%s\t%s\n", "Enabled routes", t)
} else {
fmt.Fprintf(w, "%s\t%s\n", "", t)
}
}
if msg.AdvertisedExitNode {
if msg.EnabledExitNode {
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "enabled")
} else {
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "disabled")
}
} else {
fmt.Fprintf(w, "%s\t%s\n", "Exit node", "no")
}
}