feat: add command to set name of a machine

This commit is contained in:
Johan Siebens
2025-02-15 15:07:14 +01:00
parent 48bd29beba
commit 28c5ff2570
28 changed files with 1624 additions and 2597 deletions
+35
View File
@@ -33,6 +33,7 @@ func machineCommands() *cobra.Command {
command.AddCommand(disableExitNodeCommand())
command.AddCommand(disableMachineKeyExpiryCommand())
command.AddCommand(authorizeMachineCommand())
command.AddCommand(setMachineNameCommand())
return command
}
@@ -168,6 +169,40 @@ func deleteMachineCommand() *cobra.Command {
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",
@@ -0,0 +1,24 @@
package migration
import (
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
func m202502150830_use_hostname() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "202502150830",
Migrate: func(db *gorm.DB) error {
type Machine struct {
UseOSHostname bool `gorm:"default:true"`
}
if err := db.Migrator().AddColumn(&Machine{}, "UseOSHostname"); err != nil {
return err
}
return nil
},
Rollback: nil,
}
}
@@ -21,6 +21,7 @@ func Migrations() []*gormigrate.Migration {
m202401061400_machine_indeces(),
m202402120800_user_last_authenticated(),
m202403130830_json_to_text(),
m202502150830_use_hostname(),
}
return migrations
}
+2 -1
View File
@@ -42,6 +42,7 @@ type Machine struct {
Tags Tags
KeyExpiryDisabled bool
Authorized bool
UseOSHostname bool `gorm:"default:true"`
HostInfo HostInfo
Endpoints Endpoints
@@ -124,7 +125,7 @@ func (m *Machine) IsAllowedExitNode() bool {
}
func (m *Machine) AdvertisedPrefixes() []string {
result := []string{}
var result []string
for _, r := range m.HostInfo.RoutableIPs {
if r.Bits() != 0 {
result = append(result, r.String())
+2 -1
View File
@@ -482,6 +482,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form
ID: util.NextID(),
Name: sanitizeHostname,
NameIdx: nameIdx,
UseOSHostname: true,
MachineKey: machineKey,
NodeKey: nodeKey,
Ephemeral: ephemeral || req.Ephemeral,
@@ -511,7 +512,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form
tags := append(registeredTags, advertisedTags...)
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
if m.Name != sanitizeHostname {
if m.UseOSHostname && m.Name != sanitizeHostname {
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
if err != nil {
return logError(err)
+17
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/jsiebens/ionscale/internal/config"
"github.com/jsiebens/ionscale/internal/core"
"github.com/jsiebens/ionscale/internal/domain"
@@ -15,6 +16,7 @@ import (
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/dnsname"
"time"
)
@@ -80,12 +82,27 @@ func (h *PollNetMapHandler) handlePollNetMap(c echo.Context, m *domain.Machine,
return logError(err)
}
fmt.Println("======================================================")
fmt.Println(mapRequest.Hostinfo.Hostname)
fmt.Println(mapRequest.Stream)
fmt.Println("======================================================")
if !mapRequest.Stream {
m.HostInfo = domain.HostInfo(*mapRequest.Hostinfo)
m.DiscoKey = mapRequest.DiscoKey.String()
m.Endpoints = mapRequest.Endpoints
m.LastSeen = &now
sanitizeHostname := dnsname.SanitizeHostname(m.HostInfo.Hostname)
if m.UseOSHostname && m.Name != sanitizeHostname {
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
if err != nil {
return logError(err)
}
m.Name = sanitizeHostname
m.NameIdx = nameIdx
}
if err := h.repository.SaveMachine(ctx, m); err != nil {
return logError(err)
}
+3 -3
View File
@@ -86,14 +86,13 @@ func (h *RegistrationHandlers) Register(c echo.Context) error {
}
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
if m.Name != sanitizeHostname {
if m.UseOSHostname && m.Name != sanitizeHostname {
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
if err != nil {
return logError(err)
}
m.Name = sanitizeHostname
m.NameIdx = nameIdx
}
advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags)
@@ -196,6 +195,7 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, ma
ID: util.NextID(),
Name: sanitizeHostname,
NameIdx: nameIdx,
UseOSHostname: true,
MachineKey: machineKey,
NodeKey: nodeKey,
Ephemeral: authKey.Ephemeral || req.Ephemeral,
@@ -225,7 +225,7 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, ma
m.IPv6 = domain.IP{Addr: ipv6}
} else {
sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname)
if m.Name != sanitizeHostname {
if m.UseOSHostname && m.Name != sanitizeHostname {
nameIdx, err := h.repository.GetNextMachineNameIndex(ctx, tailnet.ID, sanitizeHostname)
if err != nil {
return logError(err)
+68
View File
@@ -8,6 +8,8 @@ import (
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
"google.golang.org/protobuf/types/known/timestamppb"
"net/netip"
"strings"
"tailscale.com/util/dnsname"
"time"
)
@@ -162,6 +164,72 @@ func (s *Service) ExpireMachine(ctx context.Context, req *connect.Request[api.Ex
return connect.NewResponse(&api.ExpireMachineResponse{}), nil
}
func (s *Service) SetMachineName(ctx context.Context, req *connect.Request[api.SetMachineNameRequest]) (*connect.Response[api.SetMachineNameResponse], error) {
principal := CurrentPrincipal(ctx)
m, err := s.repository.GetMachine(ctx, req.Msg.MachineId)
if err != nil {
return nil, logError(err)
}
if m == nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("machine not found"))
}
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
}
if req.Msg.UseOsHostname {
sanitizeHostname := dnsname.SanitizeHostname(m.HostInfo.Hostname)
nameIdx, err := s.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
if err != nil {
return nil, logError(err)
}
m.Name = sanitizeHostname
m.NameIdx = nameIdx
m.UseOSHostname = true
if err := s.repository.SaveMachine(ctx, m); err != nil {
return nil, logError(err)
}
s.sessionManager.NotifyAll(m.TailnetID)
return connect.NewResponse(&api.SetMachineNameResponse{}), nil
}
if strings.TrimSpace(req.Msg.Name) == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("machine name is required when not using os hostname"))
}
sanitizeHostname := dnsname.SanitizeHostname(req.Msg.Name)
if sanitizeHostname == m.Name {
return connect.NewResponse(&api.SetMachineNameResponse{}), nil
}
nameIdx, err := s.repository.GetNextMachineNameIndex(ctx, m.TailnetID, sanitizeHostname)
if err != nil {
return nil, logError(err)
}
if nameIdx > 0 {
return nil, connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("machine name already in use"))
}
m.Name = sanitizeHostname
m.NameIdx = 0
m.UseOSHostname = false
if err := s.repository.SaveMachine(ctx, m); err != nil {
return nil, logError(err)
}
s.sessionManager.NotifyAll(m.TailnetID)
return connect.NewResponse(&api.SetMachineNameResponse{}), nil
}
func (s *Service) AuthorizeMachine(ctx context.Context, req *connect.Request[api.AuthorizeMachineRequest]) (*connect.Response[api.AuthorizeMachineResponse], error) {
principal := CurrentPrincipal(ctx)