1
0
mirror of https://github.com/juanfont/headscale.git synced 2024-12-20 19:09:07 +01:00
juanfont.headscale/cmd/headscale/cli/nodes.go

648 lines
14 KiB
Go
Raw Normal View History

package cli
import (
"fmt"
"log"
2022-09-03 23:46:14 +02:00
"net/netip"
2021-07-17 00:23:12 +02:00
"strconv"
2022-01-16 14:16:59 +01:00
"strings"
"time"
2021-07-17 11:09:42 +02:00
survey "github.com/AlecAivazis/survey/v2"
"github.com/juanfont/headscale"
2021-11-04 23:44:35 +01:00
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
2021-08-15 23:10:39 +02:00
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"
"tailscale.com/types/key"
)
2021-07-31 23:14:24 +02:00
func init() {
rootCmd.AddCommand(nodeCmd)
listNodesCmd.Flags().StringP("namespace", "n", "", "Filter by namespace")
listNodesCmd.Flags().BoolP("tags", "t", false, "Show tags")
nodeCmd.AddCommand(listNodesCmd)
registerNodeCmd.Flags().StringP("namespace", "n", "", "Namespace")
err := registerNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
registerNodeCmd.Flags().StringP("key", "k", "", "Key")
err = registerNodeCmd.MarkFlagRequired("key")
if err != nil {
log.Fatalf(err.Error())
}
2021-07-25 15:04:06 +02:00
nodeCmd.AddCommand(registerNodeCmd)
expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
2021-11-21 14:40:44 +01:00
err = expireNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(expireNodeCmd)
2022-03-13 22:03:20 +01:00
renameNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = renameNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(renameNodeCmd)
deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = deleteNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
2021-07-25 15:04:06 +02:00
nodeCmd.AddCommand(deleteNodeCmd)
moveNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
2022-05-02 06:32:33 +02:00
err = moveNodeCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
2022-05-02 06:32:33 +02:00
moveNodeCmd.Flags().StringP("namespace", "n", "", "New namespace")
2022-05-02 06:32:33 +02:00
err = moveNodeCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(moveNodeCmd)
tagCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = tagCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
tagCmd.Flags().
StringSliceP("tags", "t", []string{}, "List of tags to add to the node")
nodeCmd.AddCommand(tagCmd)
2021-07-25 15:04:06 +02:00
}
var nodeCmd = &cobra.Command{
Use: "nodes",
Short: "Manage the nodes of Headscale",
Aliases: []string{"node", "machine", "machines"},
2021-06-28 20:04:05 +02:00
}
2021-07-25 15:04:06 +02:00
var registerNodeCmd = &cobra.Command{
Use: "register",
Short: "Registers a machine to your network",
Run: func(cmd *cobra.Command, args []string) {
2021-11-04 23:44:35 +01:00
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
2021-11-04 23:44:35 +01:00
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
2021-11-04 23:44:35 +01:00
defer cancel()
defer conn.Close()
machineKey, err := cmd.Flags().GetString("key")
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Error getting node key from flag: %s", err),
2021-11-13 09:36:45 +01:00
output,
)
2021-11-14 16:46:09 +01:00
return
}
2021-11-04 23:44:35 +01:00
request := &v1.RegisterMachineRequest{
Key: machineKey,
Namespace: namespace,
}
response, err := client.RegisterMachine(ctx, request)
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf(
"Cannot register machine: %s\n",
status.Convert(err).Message(),
),
output,
)
2021-11-14 16:46:09 +01:00
return
}
2021-11-04 23:44:35 +01:00
SuccessOutput(response.Machine, "Machine register", output)
},
}
2021-07-25 15:04:06 +02:00
var listNodesCmd = &cobra.Command{
Use: "list",
Short: "List nodes",
Aliases: []string{"ls", "show"},
2021-05-01 20:00:25 +02:00
Run: func(cmd *cobra.Command, args []string) {
2021-11-04 23:44:35 +01:00
output, _ := cmd.Flags().GetString("output")
namespace, err := cmd.Flags().GetString("namespace")
2021-05-01 20:00:25 +02:00
if err != nil {
2021-11-04 23:44:35 +01:00
ErrorOutput(err, fmt.Sprintf("Error getting namespace: %s", err), output)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
2021-05-01 20:00:25 +02:00
}
showTags, err := cmd.Flags().GetBool("tags")
if err != nil {
ErrorOutput(err, fmt.Sprintf("Error getting tags flag: %s", err), output)
return
}
2021-05-01 20:00:25 +02:00
ctx, client, conn, cancel := getHeadscaleCLIClient()
2021-11-04 23:44:35 +01:00
defer cancel()
defer conn.Close()
2021-09-02 17:06:47 +02:00
2021-11-04 23:44:35 +01:00
request := &v1.ListMachinesRequest{
Namespace: namespace,
2021-09-03 10:23:45 +02:00
}
2021-11-04 23:44:35 +01:00
response, err := client.ListMachines(ctx, request)
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()),
output,
)
2021-11-14 16:46:09 +01:00
2021-05-08 13:58:51 +02:00
return
}
2021-11-04 23:44:35 +01:00
if output != "" {
SuccessOutput(response.Machines, "", output)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
2021-05-01 20:00:25 +02:00
}
tableData, err := nodesToPtables(namespace, showTags, response.Machines)
2021-08-15 23:10:39 +02:00
if err != nil {
2021-11-04 23:44:35 +01:00
ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
2021-05-01 20:00:25 +02:00
}
err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
2021-08-15 23:35:03 +02:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Failed to render pterm table: %s", err),
output,
)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
2021-08-15 23:35:03 +02:00
}
2021-05-01 20:00:25 +02:00
},
}
2021-07-17 00:23:12 +02:00
2021-11-21 14:40:44 +01:00
var expireNodeCmd = &cobra.Command{
Use: "expire",
Short: "Expire (log out) a machine in your network",
2021-11-21 22:35:36 +01:00
Long: "Expiring a node will keep the node in the database and force it to reauthenticate.",
Aliases: []string{"logout", "exp", "e"},
2021-11-21 14:40:44 +01:00
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
2021-11-21 22:34:03 +01:00
identifier, err := cmd.Flags().GetUint64("identifier")
2021-11-21 14:40:44 +01:00
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
request := &v1.ExpireMachineRequest{
2021-11-21 22:34:03 +01:00
MachineId: identifier,
2021-11-21 14:40:44 +01:00
}
response, err := client.ExpireMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Cannot expire machine: %s\n",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Machine, "Machine expired", output)
},
}
2022-03-13 22:03:20 +01:00
var renameNodeCmd = &cobra.Command{
2022-04-24 21:57:15 +02:00
Use: "rename NEW_NAME",
Short: "Renames a machine in your network",
2022-03-13 22:03:20 +01:00
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
2022-03-13 22:10:41 +01:00
newName := ""
if len(args) > 0 {
newName = args[0]
}
2022-03-13 22:03:20 +01:00
request := &v1.RenameMachineRequest{
MachineId: identifier,
2022-04-24 21:57:15 +02:00
NewName: newName,
2022-03-13 22:03:20 +01:00
}
response, err := client.RenameMachine(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
2022-05-16 20:29:31 +02:00
"Cannot rename machine: %s\n",
2022-03-13 22:03:20 +01:00
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(response.Machine, "Machine renamed", output)
},
}
2021-07-25 15:04:06 +02:00
var deleteNodeCmd = &cobra.Command{
Use: "delete",
Short: "Delete a node",
Aliases: []string{"del"},
2021-07-17 00:23:12 +02:00
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
2021-11-04 23:44:35 +01:00
2021-11-21 22:34:03 +01:00
identifier, err := cmd.Flags().GetUint64("identifier")
2021-07-17 00:23:12 +02:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
2021-07-17 00:23:12 +02:00
}
2021-11-04 23:44:35 +01:00
ctx, client, conn, cancel := getHeadscaleCLIClient()
2021-11-04 23:44:35 +01:00
defer cancel()
defer conn.Close()
getRequest := &v1.GetMachineRequest{
2021-11-21 22:34:03 +01:00
MachineId: identifier,
2021-11-04 23:44:35 +01:00
}
getResponse, err := client.GetMachine(ctx, getRequest)
2021-07-17 00:23:12 +02:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf(
"Error getting node node: %s",
status.Convert(err).Message(),
),
output,
)
2021-11-14 16:46:09 +01:00
2021-11-04 23:44:35 +01:00
return
}
deleteRequest := &v1.DeleteMachineRequest{
MachineId: identifier,
2021-07-17 00:23:12 +02:00
}
2021-07-17 11:09:42 +02:00
confirm := false
force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
2021-11-13 09:36:45 +01:00
Message: fmt.Sprintf(
"Do you want to remove the node %s?",
getResponse.GetMachine().Name,
),
}
err = survey.AskOne(prompt, &confirm)
if err != nil {
return
}
}
if confirm || force {
2021-11-04 23:44:35 +01:00
response, err := client.DeleteMachine(ctx, deleteRequest)
if output != "" {
SuccessOutput(response, "", output)
2021-11-14 16:46:09 +01:00
return
}
2021-07-17 11:09:42 +02:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf(
"Error deleting node: %s",
status.Convert(err).Message(),
),
output,
)
2021-11-14 16:46:09 +01:00
return
}
2021-11-13 09:36:45 +01:00
SuccessOutput(
map[string]string{"Result": "Node deleted"},
"Node deleted",
output,
)
2021-11-04 23:44:35 +01:00
} else {
SuccessOutput(map[string]string{"Result": "Node not deleted"}, "Node not deleted", output)
2021-07-17 00:23:12 +02:00
}
},
}
2021-08-15 23:10:39 +02:00
var moveNodeCmd = &cobra.Command{
Use: "move",
Short: "Move node to another namespace",
Aliases: []string{"mv"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error getting namespace: %s", err),
output,
)
return
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
getRequest := &v1.GetMachineRequest{
MachineId: identifier,
}
_, err = client.GetMachine(ctx, getRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error getting node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
moveRequest := &v1.MoveMachineRequest{
MachineId: identifier,
Namespace: namespace,
}
moveResponse, err := client.MoveMachine(ctx, moveRequest)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf(
"Error moving node: %s",
status.Convert(err).Message(),
),
output,
)
return
}
SuccessOutput(moveResponse.Machine, "Node moved to another namespace", output)
},
}
2021-11-13 09:36:45 +01:00
func nodesToPtables(
currentNamespace string,
showTags bool,
2021-11-13 09:36:45 +01:00
machines []*v1.Machine,
) (pterm.TableData, error) {
tableHeader := []string{
"ID",
2022-07-22 19:33:11 +02:00
"Hostname",
"Name",
"NodeKey",
"Namespace",
"IP addresses",
"Ephemeral",
"Last seen",
"Online",
"Expired",
2021-11-13 09:36:45 +01:00
}
if showTags {
tableHeader = append(tableHeader, []string{
"ForcedTags",
"InvalidTags",
"ValidTags",
}...)
2021-11-13 09:36:45 +01:00
}
tableData := pterm.TableData{tableHeader}
2021-08-15 23:10:39 +02:00
for _, machine := range machines {
2021-08-15 23:10:39 +02:00
var ephemeral bool
2021-11-04 23:44:35 +01:00
if machine.PreAuthKey != nil && machine.PreAuthKey.Ephemeral {
2021-08-15 23:10:39 +02:00
ephemeral = true
}
2021-08-15 23:10:39 +02:00
var lastSeen time.Time
var lastSeenTime string
2021-09-10 00:37:01 +02:00
if machine.LastSeen != nil {
2021-11-04 23:44:35 +01:00
lastSeen = machine.LastSeen.AsTime()
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
2021-08-15 23:10:39 +02:00
}
var expiry time.Time
if machine.Expiry != nil {
expiry = machine.Expiry.AsTime()
}
var nodeKey key.NodePublic
err := nodeKey.UnmarshalText(
[]byte(headscale.NodePublicKeyEnsurePrefix(machine.NodeKey)),
)
2021-08-15 23:10:39 +02:00
if err != nil {
return nil, err
}
var online string
2021-11-13 09:36:45 +01:00
if lastSeen.After(
time.Now().Add(-5 * time.Minute),
) { // TODO: Find a better way to reliably show if online
online = pterm.LightGreen("online")
} else {
online = pterm.LightRed("offline")
}
var expired string
if expiry.IsZero() || expiry.After(time.Now()) {
expired = pterm.LightGreen("no")
2021-08-15 23:10:39 +02:00
} else {
expired = pterm.LightRed("yes")
2021-08-15 23:10:39 +02:00
}
2021-09-02 17:06:47 +02:00
var forcedTags string
2022-04-16 13:32:00 +02:00
for _, tag := range machine.ForcedTags {
forcedTags += "," + tag
2022-04-16 13:32:00 +02:00
}
forcedTags = strings.TrimLeft(forcedTags, ",")
var invalidTags string
2022-04-16 13:32:00 +02:00
for _, tag := range machine.InvalidTags {
if !contains(machine.ForcedTags, tag) {
invalidTags += "," + pterm.LightRed(tag)
2022-04-16 13:32:00 +02:00
}
}
invalidTags = strings.TrimLeft(invalidTags, ",")
var validTags string
2022-04-16 13:32:00 +02:00
for _, tag := range machine.ValidTags {
if !contains(machine.ForcedTags, tag) {
validTags += "," + pterm.LightGreen(tag)
2022-04-16 13:32:00 +02:00
}
}
validTags = strings.TrimLeft(validTags, ",")
2022-04-16 13:32:00 +02:00
2021-09-02 17:06:47 +02:00
var namespace string
if currentNamespace == "" || (currentNamespace == machine.Namespace.Name) {
namespace = pterm.LightMagenta(machine.Namespace.Name)
2021-09-02 17:06:47 +02:00
} else {
// Shared into this namespace
namespace = pterm.LightYellow(machine.Namespace.Name)
2021-09-02 17:06:47 +02:00
}
2022-05-08 21:06:12 +02:00
2022-05-16 14:59:46 +02:00
var IPV4Address string
var IPV6Address string
2022-05-08 21:21:10 +02:00
for _, addr := range machine.IpAddresses {
2022-09-03 23:46:14 +02:00
if netip.MustParseAddr(addr).Is4() {
2022-05-16 14:59:46 +02:00
IPV4Address = addr
2022-05-08 21:21:10 +02:00
} else {
2022-05-16 14:59:46 +02:00
IPV6Address = addr
2022-05-08 21:21:10 +02:00
}
2022-05-08 21:06:12 +02:00
}
nodeData := []string{
strconv.FormatUint(machine.Id, headscale.Base10),
machine.Name,
2022-07-22 19:33:11 +02:00
machine.GetGivenName(),
nodeKey.ShortString(),
namespace,
2022-05-16 14:59:46 +02:00
strings.Join([]string{IPV4Address, IPV6Address}, ", "),
strconv.FormatBool(ephemeral),
lastSeenTime,
online,
expired,
}
if showTags {
nodeData = append(nodeData, []string{forcedTags, invalidTags, validTags}...)
}
tableData = append(
tableData,
nodeData,
2021-11-04 23:44:35 +01:00
)
2021-08-15 23:10:39 +02:00
}
2021-11-14 16:46:09 +01:00
return tableData, nil
2021-08-15 23:10:39 +02:00
}
var tagCmd = &cobra.Command{
Use: "tag",
Short: "Manage the tags of a node",
Aliases: []string{"tags", "t"},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
ctx, client, conn, cancel := getHeadscaleCLIClient()
defer cancel()
defer conn.Close()
// retrieve flags from CLI
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error converting ID to integer: %s", err),
output,
)
return
}
tagsToSet, err := cmd.Flags().GetStringSlice("tags")
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error retrieving list of tags to add to machine, %v", err),
output,
)
return
}
// Sending tags to machine
request := &v1.SetTagsRequest{
MachineId: identifier,
Tags: tagsToSet,
}
resp, err := client.SetTags(ctx, request)
if err != nil {
ErrorOutput(
err,
fmt.Sprintf("Error while sending tags to headscale: %s", err),
output,
)
2022-05-13 10:17:52 +02:00
return
}
if resp != nil {
SuccessOutput(
resp.GetMachine(),
"Machine updated",
output,
)
}
},
}