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

537 lines
12 KiB
Go
Raw Normal View History

package cli
import (
"fmt"
"log"
2021-07-17 00:23:12 +02:00
"strconv"
"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")
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)
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)
shareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = shareMachineCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
shareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = shareMachineCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(shareMachineCmd)
unshareMachineCmd.Flags().StringP("namespace", "n", "", "Namespace")
err = unshareMachineCmd.MarkFlagRequired("namespace")
if err != nil {
log.Fatalf(err.Error())
}
unshareMachineCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)")
err = unshareMachineCmd.MarkFlagRequired("identifier")
if err != nil {
log.Fatalf(err.Error())
}
nodeCmd.AddCommand(unshareMachineCmd)
2021-07-25 15:04:06 +02:00
}
var nodeCmd = &cobra.Command{
2021-06-28 20:04:05 +02:00
Use: "nodes",
Short: "Manage the nodes of Headscale",
}
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 machine key from flag: %s", err),
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{
2021-05-01 20:00:25 +02:00
Use: "list",
Short: "List nodes",
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
}
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, 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.",
2021-11-21 14:40:44 +01:00
Aliases: []string{"logout"},
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)
},
}
2021-07-25 15:04:06 +02:00
var deleteNodeCmd = &cobra.Command{
Use: "delete",
2021-07-17 00:23:12 +02:00
Short: "Delete a node",
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
2021-11-04 23:44:35 +01:00
func sharingWorker(
cmd *cobra.Command,
) (string, *v1.Machine, *v1.Namespace, error) {
output, _ := cmd.Flags().GetString("output")
namespaceStr, 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 "", nil, nil, err
}
ctx, client, conn, cancel := getHeadscaleCLIClient()
2021-11-04 23:44:35 +01:00
defer cancel()
defer conn.Close()
2021-11-21 22:34:03 +01:00
identifier, err := cmd.Flags().GetUint64("identifier")
if err != nil {
2021-11-04 23:44:35 +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 "", nil, nil, err
}
2021-11-04 23:44:35 +01:00
machineRequest := &v1.GetMachineRequest{
2021-11-21 22:34:03 +01:00
MachineId: identifier,
}
2021-11-04 23:44:35 +01:00
machineResponse, err := client.GetMachine(ctx, machineRequest)
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 "", nil, nil, err
}
2021-11-04 23:44:35 +01:00
namespaceRequest := &v1.GetNamespaceRequest{
Name: namespaceStr,
}
namespaceResponse, err := client.GetNamespace(ctx, namespaceRequest)
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 "", nil, nil, err
}
2021-11-04 23:44:35 +01:00
return output, machineResponse.GetMachine(), namespaceResponse.GetNamespace(), nil
}
var shareMachineCmd = &cobra.Command{
Use: "share",
2021-09-03 10:23:45 +02:00
Short: "Shares a node from the current namespace to the specified one",
Run: func(cmd *cobra.Command, args []string) {
2021-11-14 18:03:21 +01:00
output, machine, namespace, err := sharingWorker(cmd)
2021-11-04 23:44:35 +01:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
output,
)
2021-11-14 16:46:09 +01:00
2021-09-03 10:23:45 +02:00
return
}
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()
request := &v1.ShareMachineRequest{
MachineId: machine.Id,
Namespace: namespace.Name,
}
response, err := client.ShareMachine(ctx, request)
2021-09-03 10:23:45 +02:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Error sharing node: %s", status.Convert(err).Message()),
output,
)
2021-11-14 16:46:09 +01:00
2021-09-03 10:23:45 +02:00
return
}
2021-11-04 23:44:35 +01:00
SuccessOutput(response.Machine, "Node shared", output)
2021-09-03 10:23:45 +02:00
},
}
var unshareMachineCmd = &cobra.Command{
Use: "unshare",
Short: "Unshares a node from the specified namespace",
Run: func(cmd *cobra.Command, args []string) {
2021-11-14 18:03:21 +01:00
output, machine, namespace, err := sharingWorker(cmd)
2021-11-04 23:44:35 +01:00
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Failed to fetch namespace or machine: %s", err),
output,
)
2021-11-14 16:46:09 +01:00
return
}
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()
request := &v1.UnshareMachineRequest{
MachineId: machine.Id,
Namespace: namespace.Name,
}
response, err := client.UnshareMachine(ctx, request)
if err != nil {
2021-11-13 09:36:45 +01:00
ErrorOutput(
err,
fmt.Sprintf("Error unsharing node: %s", status.Convert(err).Message()),
output,
)
2021-11-14 16:46:09 +01:00
return
}
2021-11-07 10:57:39 +01:00
SuccessOutput(response.Machine, "Node unshared", output)
},
}
2021-11-13 09:36:45 +01:00
func nodesToPtables(
currentNamespace string,
machines []*v1.Machine,
) (pterm.TableData, error) {
tableData := pterm.TableData{
2021-11-13 09:36:45 +01:00
{
"ID",
"Name",
"NodeKey",
"Namespace",
"IP address",
"Ephemeral",
"Last seen",
"Online",
"Expired",
2021-11-13 09:36:45 +01:00
},
}
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 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
}
tableData = append(
tableData,
2021-11-04 23:44:35 +01:00
[]string{
strconv.FormatUint(machine.Id, headscale.Base10),
2021-11-04 23:44:35 +01:00
machine.Name,
nodeKey.ShortString(),
namespace,
machine.IpAddress,
strconv.FormatBool(ephemeral),
lastSeenTime,
online,
expired,
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
}