diff --git a/cmd/admin/v2/commands.go b/cmd/admin/v2/commands.go index 3aad5c0..6bd3dec 100644 --- a/cmd/admin/v2/commands.go +++ b/cmd/admin/v2/commands.go @@ -18,6 +18,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { adminCmd.AddCommand(newComponentCmd(c)) adminCmd.AddCommand(newImageCmd(c)) adminCmd.AddCommand(newIPCmd(c)) + adminCmd.AddCommand(newMachineCmd(c)) adminCmd.AddCommand(newNetworkCmd(c)) adminCmd.AddCommand(newPartitionCmd(c)) adminCmd.AddCommand(newProjectCmd(c)) diff --git a/cmd/admin/v2/machine.go b/cmd/admin/v2/machine.go new file mode 100644 index 0000000..016eb0d --- /dev/null +++ b/cmd/admin/v2/machine.go @@ -0,0 +1,648 @@ +package v2 + +import ( + "context" + "fmt" + "net/netip" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/metal-stack/api/go/errorutil" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/cli/cmd/sorters" + "github.com/metal-stack/cli/pkg/helpers" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" + metalssh "github.com/metal-stack/metal-lib/pkg/ssh" + metalvpn "github.com/metal-stack/metal-lib/pkg/vpn" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type machine struct { + c *config.Config +} + +func newMachineCmd(c *config.Config) *cobra.Command { + w := &machine{ + c: c, + } + + cmdsConfig := &genericcli.CmdsConfig[*apiv2.MachineServiceCreateRequest, *apiv2.MachineServiceUpdateRequest, *apiv2.Machine]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "machine", + Aliases: []string{"ms"}, + Plural: "machines", + Description: "manage machines", + Sorter: sorters.MachineSorter(), + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "id of machine which should be listed") + cmd.Flags().String("name", "", "name from machines which should be listed") + cmd.Flags().String("hostname", "", "hostname from machines which should be listed") + cmd.Flags().String("size", "", "size from machines which should be listed") + cmd.Flags().String("image", "", "image") + cmd.Flags().StringP("project", "p", "", "project from where machines should be listed") + cmd.Flags().StringP("partition", "", "", "partition from where machines should be listed") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.Completion.Size)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("image", c.Completion.Image)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.Partition)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("id", c.Completion.AdminMachine)) + + cmd.Long = cmd.Short + "\n" + helpers.EmojiHelpText() + }, + DescribeCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project of the machine") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + }, + CreateRequestFromCLI: func() (*apiv2.MachineServiceCreateRequest, error) { + rq, err := helpers.MachineCreateRequestFromCLI(c) + if err != nil { + return nil, err + } + + if viper.IsSet("id") { + rq.Uuid = new(viper.GetString("id")) + } + + return rq, nil + }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + helpers.AddMachineCreateFlags(cmd, "machine", c.Completion) + + cmd.Flags().String("id", "", "id of the machine to create. [optional]") + genericcli.Must(cmd.RegisterFlagCompletionFunc("id", c.Completion.AdminMachine)) + + cmd.Aliases = []string{"allocate"} + cmd.Example = `machine create can be done in two different ways: + +- default with automatic allocation: + + metalctl machine create \ + --hostname worker01 \ + --name worker \ + --image ubuntu-18.04 \ # query available with: metalctl image list + --size t1-small-x86 \ # query available with: metalctl size list + --partition test \ # query available with: metalctl partition list + --project cluster01 \ + --sshpublickey "@~/.ssh/id_rsa.pub" + +- for metal administration with reserved machines: + + reserve a machine you want to allocate: + + metalctl machine reserve 00000000-0000-0000-0000-0cc47ae54694 --description "blocked for maintenance" + + allocate this machine: + + metalctl machine create \ + --hostname worker01 \ + --name worker \ + --image ubuntu-18.04 \ # query available with: metalctl image list + --project cluster01 \ + --sshpublickey "@~/.ssh/id_rsa.pub" \ + --id 00000000-0000-0000-0000-0cc47ae54694 + +after you do not want to use this machine exclusive, remove the reservation: + +metalctl machine reserve 00000000-0000-0000-0000-0cc47ae54694 --remove + +Once created the machine installation can not be modified anymore. +` + }, + DeleteCmdMutateFn: func(cmd *cobra.Command) { + cmd.Short = "Delete a machine from the database. This can only be done if the machine is offline and dead." + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project from where machines should be listed") + cmd.Flags().String("description", "", "description of the machine") + cmd.Flags().StringSlice("labels", nil, "labels to replace for the machine") + cmd.Flags().StringSlice("add-labels", nil, "labels to add to the machine") + cmd.Flags().StringSlice("remove-labels", nil, "labels to remove to the machine") + cmd.Flags().StringP("ssh-public-key", "i", "", + `SSH public key for access via ssh and console. [optional] +Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". +If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order.`) + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + }, + UpdateRequestFromCLI: func(args []string) (*apiv2.MachineServiceUpdateRequest, error) { + return helpers.MachineUpdateRequestFromCLI(c, args) + }, + ValidArgsFn: c.Completion.AdminMachine, + } + + bmcCommandCmd := &cobra.Command{ + Use: "bmc-command", + Short: "send a command to the bmc of a machine", + RunE: func(cmd *cobra.Command, args []string) error { + return w.bmcCommand(args) + }, + ValidArgsFunction: c.Completion.AdminMachine, + } + bmcCommandCmd.Flags().String("command", "", "the actual command to send to the machine") + genericcli.Must(bmcCommandCmd.RegisterFlagCompletionFunc("command", c.Completion.BMCCommands)) + genericcli.Must(bmcCommandCmd.MarkFlagRequired("command")) + + bmcCmd := &cobra.Command{ + Use: "bmc", + Aliases: []string{"ipmi"}, + Short: "get and list machine bmc/ipmi information", + } + + bmcGetCmd := &cobra.Command{ + Use: "get", + Short: "get the bmc of a machine", + RunE: func(cmd *cobra.Command, args []string) error { + return w.bmcGet(args) + }, + ValidArgsFunction: c.Completion.AdminMachine, + } + + bmcListCmd := &cobra.Command{ + Use: "list", + Short: "list the bmc of machines", + RunE: func(cmd *cobra.Command, args []string) error { + return w.bmcList() + }, + } + bmcListCmd.Flags().String("id", "", "id of machine which should be listed") + bmcListCmd.Flags().String("size", "", "size from machines which should be listed") + bmcListCmd.Flags().String("image", "", "image") + bmcListCmd.Flags().StringP("project", "p", "", "project from where machines should be listed") + bmcListCmd.Flags().StringP("partition", "", "", "partition from where machines should be listed") + + genericcli.Must(bmcListCmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + genericcli.Must(bmcListCmd.RegisterFlagCompletionFunc("size", c.Completion.Size)) + genericcli.Must(bmcListCmd.RegisterFlagCompletionFunc("image", c.Completion.Image)) + genericcli.Must(bmcListCmd.RegisterFlagCompletionFunc("partition", c.Completion.Partition)) + genericcli.Must(bmcListCmd.RegisterFlagCompletionFunc("id", c.Completion.AdminMachine)) + + bmcCmd.AddCommand(bmcGetCmd, bmcListCmd) + + lockCmd := &cobra.Command{ + Use: "lock", + Short: "lock or unlock a machine, e.g. machine cannot be used", + RunE: func(cmd *cobra.Command, args []string) error { + return w.lockOrTaint(args, apiv2.MachineState_MACHINE_STATE_LOCKED) + }, + ValidArgsFunction: c.Completion.AdminMachine, + } + lockCmd.Flags().String("description", "", "description of why the machine was locked") + lockCmd.Flags().Bool("remove", false, "if set to true, machine will be unlocked") + + taintCmd := &cobra.Command{ + Use: "taint", + Short: "taint or untaint a machine, e.g. machine will not be automatically selected on machine create, only admins can create them", + RunE: func(cmd *cobra.Command, args []string) error { + return w.lockOrTaint(args, apiv2.MachineState_MACHINE_STATE_TAINTED) + }, + ValidArgsFunction: c.Completion.AdminMachine, + } + taintCmd.Flags().String("description", "", "description of why the machine was tainted") + taintCmd.Flags().Bool("remove", false, "if set to true, machine will be untainted") + + consoleCmd := &cobra.Command{ + Use: "console", + Short: "establishes a connection to the serial console of a machine. for authentication at the metal-console it uses the token such that no machine ssh key is required for access (unlike the corresponding user API command).", + RunE: func(cmd *cobra.Command, args []string) error { + return w.console(cmd.Context(), args) + }, + ValidArgsFunction: c.Completion.AdminMachine, + } + consoleCmd.Flags().Bool("ipmi", false, "if set to true, the serial console will be opened using ipmitool (requires ipmitool to be present)") + consoleCmd.Flags().Int("metal-console-port", 5222, "port open on our control-plane to connect via ssh to get machine console access") + + firewallSSHCmd := &cobra.Command{ + Use: "ssh ", + Short: "SSH to a firewall", + Long: `SSH to a firewall via VPN.`, + RunE: func(cmd *cobra.Command, args []string) error { + return w.firewallSSH(cmd.Context(), args) + }, + ValidArgsFunction: c.Completion.Firewall, + } + firewallSSHCmd.Flags().StringP("identity", "i", "~/.ssh/id_rsa", "specify identity file to SSH to the firewall like: -i path/to/id_rsa") + firewallSSHCmd.Flags().String("reason", "", "the reason why to connect to the firewall through SSH") + + return genericcli.NewCmds(cmdsConfig, bmcCommandCmd, bmcCmd, lockCmd, taintCmd, consoleCmd, firewallSSHCmd) +} + +func (c *machine) Create(rq *apiv2.MachineServiceCreateRequest) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Machine().Create(ctx, rq) + if err != nil { + if errorutil.IsConflict(err) { + return nil, genericcli.AlreadyExistsError() + } + + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) Delete(id string) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Machine().Delete(ctx, &adminv2.MachineServiceDeleteRequest{ + Uuid: id, + }) + if err != nil { + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) Get(id string) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Machine().Get(ctx, &adminv2.MachineServiceGetRequest{ + Uuid: id, + }) + if err != nil { + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) List() ([]*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + var allocation *apiv2.MachineAllocationQuery + + if viper.IsSet("hostname") || viper.IsSet("name") || viper.IsSet("project") || viper.IsSet("image") { + allocation = &apiv2.MachineAllocationQuery{ + Hostname: pointer.PointerOrNil(viper.GetString("hostname")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Project: pointer.PointerOrNil(viper.GetString("project")), + Image: pointer.PointerOrNil(viper.GetString("image")), + } + } + + resp, err := c.c.Client.Adminv2().Machine().List(ctx, &adminv2.MachineServiceListRequest{ + Query: &apiv2.MachineQuery{ + Uuid: pointer.PointerOrNil(viper.GetString("id")), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Size: pointer.PointerOrNil(viper.GetString("size")), + Allocation: allocation, + // Rack: pointer.PointerOrNil(viper.GetString("rack")), + // Labels: &apiv2.Labels{ + // Labels: tag.NewTagMap(viper.GetStringSlice("labels")), + // }, + // Bmc: &apiv2.MachineBMCQuery{ + // Address: pointer.PointerOrNil(viper.GetString("bmc-address")), + // Mac: pointer.PointerOrNil(viper.GetString("bmc-mac")), + // User: pointer.PointerOrNil(viper.GetString("bmc-user")), + // Interface: pointer.PointerOrNil(viper.GetString("bmc-interface")), + // }, + // Fru: &apiv2.MachineFRUQuery{ + // ChassisPartNumber: pointer.PointerOrNil(viper.GetString("chassis-part-number")), + // ChassisPartSerial: pointer.PointerOrNil(viper.GetString("chassis-part-serial")), + // BoardMfg: pointer.PointerOrNil(viper.GetString("board-mfg")), + // BoardSerial: pointer.PointerOrNil(viper.GetString("board-serial")), + // BoardPartNumber: pointer.PointerOrNil(viper.GetString("board-part-number")), + // ProductManufacturer: pointer.PointerOrNil(viper.GetString("product-manufacturer")), + // ProductPartNumber: pointer.PointerOrNil(viper.GetString("product-part-number")), + // ProductSerial: pointer.PointerOrNil(viper.GetString("product-serial")), + // }, + // Hardware: &apiv2.MachineHardwareQuery{ + // Memory: pointer.PointerOrNil(viper.GetUint64("memory")), + // CpuCores: pointer.PointerOrNil(viper.GetUint32("cpu-cores")), + // }, + // State: &0, + }, + }) + if err != nil { + return nil, err + } + + return resp.Machines, nil +} + +func (c *machine) Update(rq *apiv2.MachineServiceUpdateRequest) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Machine().Update(ctx, rq) + if err != nil { + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) Convert(r *apiv2.Machine) (string, *apiv2.MachineServiceCreateRequest, *apiv2.MachineServiceUpdateRequest, error) { + update, err := helpers.MachineResponseToUpdate(r) + if err != nil { + return "", nil, nil, err + } + + create, err := helpers.MachineResponseToCreate(r) + if err != nil { + return "", nil, nil, err + } + + if r.Uuid != "" { + create.Uuid = &r.Uuid + create.Partition = nil + create.Size = nil + } + + return r.Uuid, create, update, err +} + +func (c *machine) lockOrTaint(args []string, state apiv2.MachineState) error { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return err + } + + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + if viper.GetBool("remove") { + state = apiv2.MachineState_MACHINE_STATE_AVAILABLE + } + + resp, err := c.c.Client.Adminv2().Machine().SetState(ctx, &adminv2.MachineServiceSetStateRequest{ + Uuid: id, + Description: viper.GetString("description"), + State: state, + }) + if err != nil { + return err + } + + return c.c.ListPrinter.Print(resp.Machine) +} + +func (c *machine) bmcCommand(args []string) error { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return err + } + + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + commandString := viper.GetString("command") + + cmd, ok := apiv2.MachineBMCCommand_value[commandString] + if !ok { + return fmt.Errorf("unknown bmc command: %s", commandString) + } + + _, err = c.c.Client.Adminv2().Machine().BMCCommand(ctx, &adminv2.MachineServiceBMCCommandRequest{ + Uuid: id, + Command: apiv2.MachineBMCCommand(cmd), + }) + if err != nil { + return err + } + + return nil +} + +func (c *machine) bmcGet(args []string) error { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return err + } + + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Machine().GetBMC(ctx, &adminv2.MachineServiceGetBMCRequest{ + Uuid: id, + }) + if err != nil { + return err + } + + return c.c.DescribePrinter.Print(resp) +} + +func (c *machine) bmcList() error { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + // FIXME api contains the wrong query here, must be a MachineQuery instead of a MachineBMCQuery + req := &adminv2.MachineServiceListBMCRequest{} + + resp, err := c.c.Client.Adminv2().Machine().ListBMC(ctx, req) + if err != nil { + return err + } + + return c.c.ListPrinter.Print(resp.BmcReports) +} + +func (c *machine) console(ctx context.Context, args []string) error { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return err + } + + useIpmi := viper.GetBool("ipmi") + if useIpmi { + return c.impitool(ctx, id) + } + + parsedurl, err := url.Parse(pointer.SafeDeref(c.c.Context.ApiURL)) + if err != nil { + return err + } + + err = sshClient(id, viper.GetString("sshidentity"), parsedurl.Host, viper.GetInt("metal-console-port"), &c.c.Context.Token, true) + if err != nil { + return fmt.Errorf("machine console error:%w", err) + } + + return nil +} + +func (c *machine) impitool(ctx context.Context, id string) error { + path, err := exec.LookPath("ipmitool") + if err != nil { + return fmt.Errorf("unable to locate ipmitool in path") + } + + resp, err := c.c.Client.Adminv2().Machine().GetBMC(context.Background(), &adminv2.MachineServiceGetBMCRequest{ + Uuid: id, + }) + if err != nil { + return err + } + + bmc := resp.Bmc.Bmc + intf := "lanplus" + + // -I lanplus -H 192.168.2.19 -U ADMIN -P ADMIN sol activate + hostAndPort := strings.Split(bmc.Address, ":") + if len(hostAndPort) < 2 { + hostAndPort = append(hostAndPort, "623") + } + usr := bmc.User + if bmc.User == "" { + _, _ = fmt.Fprintf(c.c.Out, "no ipmi user stored, please specify with --ipmiuser\n") + } + ipmiuser := viper.GetString("ipmiuser") + if ipmiuser != "" { + usr = ipmiuser + } + password := bmc.Password + if bmc.Password == "" { + _, _ = fmt.Fprintf(c.c.Out, "no ipmi password stored, please specify with --ipmipassword\n") + } + + bmcpassword := viper.GetString("ipmipassword") + if bmcpassword != "" { + password = bmcpassword + } + + err = os.Setenv("IPMITOOL_PASSWORD", password) + if err != nil { + return err + } + defer func() { + _ = os.Unsetenv("IPMITOOL_PASSWORD") + }() + + args := []string{"-I", intf, "-H", hostAndPort[0], "-p", hostAndPort[1], "-U", usr, "-E", "sol", "activate"} + _, _ = fmt.Fprintf(c.c.Out, "connecting to console with:\n%s %s\nExit with ~.\n\n", path, strings.Join(args, " ")) + cmd := exec.CommandContext(ctx, path, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + + return cmd.Run() +} + +func (c *machine) firewallSSH(ctx context.Context, args []string) (err error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return err + } + + machine, err := c.Get(id) + if err != nil { + return fmt.Errorf("failed to find firewall: %w", err) + } + + if machine.Allocation == nil { + return fmt.Errorf("firewall allocation is nil") + } + + if machine.Allocation.AllocationType != apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL { + return fmt.Errorf("ssh can only be used for connecting to firewalls") + } + + projectID := machine.Allocation.Project + _, _ = fmt.Fprintf(c.c.Out, "accessing firewall through vpn ") + authKeyResp, err := c.c.Client.Adminv2().VPN().AuthKey(ctx, &adminv2.VPNServiceAuthKeyRequest{ + Project: projectID, + Ephemeral: true, + Reason: viper.GetString("reason"), + }) + if err != nil { + return fmt.Errorf("failed to get VPN auth key: %w", err) + } + + var vpnopts = []metalvpn.ConnectOpt{} + + if machine.Allocation.Vpn != nil { + for _, ip := range machine.Allocation.Vpn.Ips { + parsed, err := netip.ParseAddr(ip) + if err != nil { + return err + } + if parsed.Is4() { + vpnopts = append(vpnopts, metalvpn.ConnectOptWithVpnIPAddress(ip)) + } + } + } + + v, err := metalvpn.Connect(ctx, machine.Uuid, authKeyResp.Address, authKeyResp.AuthKey, vpnopts...) + if err != nil { + return err + } + defer func() { + _ = v.Close() + }() + + privateKeyFile := viper.GetString("identity") + if strings.HasPrefix(privateKeyFile, "~/") { + home, _ := os.UserHomeDir() + privateKeyFile = filepath.Join(home, privateKeyFile[2:]) + } + + privateKey, err := os.ReadFile(privateKeyFile) + if err != nil { + return err + } + + opts := []metalssh.ConnectOpt{metalssh.ConnectOptOutputPrivateKey(privateKey)} + + s, err := metalssh.NewClientWithConnection("metal", v.TargetIP, v.Conn, opts...) + if err != nil { + return err + } + return s.Connect(nil) +} + +// sshClient opens an interactive ssh session to the host on port with user, authenticated by the key. +func sshClient(user, keyfile, host string, port int, idToken *string, passwordAuth bool) error { + var opts []metalssh.ConnectOpt + + if passwordAuth { + opts = append(opts, metalssh.ConnectOptOutputPassword(*idToken)) + } else { + if keyfile == "" { + var err error + keyfile, err = helpers.SearchSSHKey() + if err != nil { + return err + } + } + + privateKey, err := os.ReadFile(keyfile) + if err != nil { + return err + } + + opts = append(opts, metalssh.ConnectOptOutputPrivateKey(privateKey)) + } + + s, err := metalssh.NewClient(user, host, port, opts...) + if err != nil { + return err + } + + var env *metalssh.Env + + if idToken != nil { + env = &metalssh.Env{"LC_METAL_STACK_OIDC_TOKEN": *idToken} + } + + return s.Connect(env) +} diff --git a/cmd/api/v2/commands.go b/cmd/api/v2/commands.go index 32d8182..31dcfd0 100644 --- a/cmd/api/v2/commands.go +++ b/cmd/api/v2/commands.go @@ -10,6 +10,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { cmd.AddCommand(newHealthCmd(c)) cmd.AddCommand(newImageCmd(c)) cmd.AddCommand(newIPCmd(c)) + cmd.AddCommand(newMachineCmd(c)) cmd.AddCommand(newMethodsCmd(c)) cmd.AddCommand(newNetworkCmd(c)) cmd.AddCommand(newPartitionCmd(c)) diff --git a/cmd/api/v2/ip.go b/cmd/api/v2/ip.go index afe2341..9f46a0f 100644 --- a/cmd/api/v2/ip.go +++ b/cmd/api/v2/ip.go @@ -53,8 +53,9 @@ func newIPCmd(c *config.Config) *cobra.Command { cmd.Flags().StringP("project", "p", "", "project of the ip") cmd.Flags().String("name", "", "name of the ip") cmd.Flags().String("description", "", "description of the ip") - cmd.Flags().StringArray("labels", nil, "adds (or edits) the volume labels in the form of =") - cmd.Flags().StringArray("remove-labels", nil, "removes the volume labels with the given key") + cmd.Flags().StringSlice("labels", nil, "labels to replace for the ip") + cmd.Flags().StringSlice("add-labels", nil, "labels to add to the ip") + cmd.Flags().StringSlice("remove-labels", nil, "labels to remove to the ip") cmd.Flags().Bool("static", false, "make this ip static") genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) @@ -100,12 +101,18 @@ func (c *ip) updateFromCLI(args []string) (*apiv2.IPServiceUpdateRequest, error) return nil, err } + updateLabels, err := helpers.UpdateLabelsFromCLI() + if err != nil { + return nil, err + } + req := &apiv2.IPServiceUpdateRequest{ Ip: uuid, Project: c.c.GetProject(), UpdateMeta: &apiv2.UpdateMeta{ LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, }, + Labels: updateLabels, } if viper.IsSet("name") { @@ -117,27 +124,6 @@ func (c *ip) updateFromCLI(args []string) (*apiv2.IPServiceUpdateRequest, error) if viper.IsSet("static") { req.Type = pointer.PointerOrNil(ipStaticToType(viper.GetBool("static"))) } - if viper.IsSet("remove-labels") || viper.IsSet("labels") { - updates := &apiv2.LabelsPatch{} - - if viper.IsSet("remove-labels") { - updates.Remove = viper.GetStringSlice("remove-labels") - } - - if viper.IsSet("labels") { - labels, err := genericcli.LabelsToMap(viper.GetStringSlice("labels")) - if err != nil { - return nil, err - } - updates.Update = &apiv2.Labels{Labels: labels} - } - - req.Labels = &apiv2.UpdateLabels{ - Strategy: &apiv2.UpdateLabels_Patch{ - Patch: updates, - }, - } - } return req, nil } diff --git a/cmd/api/v2/machine.go b/cmd/api/v2/machine.go new file mode 100644 index 0000000..a9ff019 --- /dev/null +++ b/cmd/api/v2/machine.go @@ -0,0 +1,200 @@ +package v2 + +import ( + "github.com/metal-stack/api/go/errorutil" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/cli/cmd/sorters" + "github.com/metal-stack/cli/pkg/helpers" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type machine struct { + c *config.Config +} + +func newMachineCmd(c *config.Config) *cobra.Command { + w := &machine{ + c: c, + } + + cmdsConfig := &genericcli.CmdsConfig[*apiv2.MachineServiceCreateRequest, *apiv2.MachineServiceUpdateRequest, *apiv2.Machine]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "machine", + Aliases: []string{"ms"}, + Plural: "machines", + Description: "allocate a machine", + Sorter: sorters.MachineSorter(), + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + CreateRequestFromCLI: func() (*apiv2.MachineServiceCreateRequest, error) { + return helpers.MachineCreateRequestFromCLI(c) + }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + helpers.AddMachineCreateFlags(cmd, "machine", c.Completion) + cmd.Aliases = []string{"allocate"} + }, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("uuid", "", "allocation uuid of machine which should be listed") + cmd.Flags().String("name", "", "name from machines which should be listed") + cmd.Flags().String("hostname", "", "hostname from machines which should be listed") + cmd.Flags().String("size", "", "size from machines which should be listed") + cmd.Flags().String("image", "", "image") + cmd.Flags().StringP("project", "p", "", "project from where machines should be listed") + cmd.Flags().StringP("partition", "", "", "partition from where machines should be listed") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.Completion.Size)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("image", c.Completion.Image)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.Partition)) + + cmd.Long = cmd.Short + "\n" + helpers.EmojiHelpText() + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project from where machines should be listed") + cmd.Flags().String("description", "", "description of the machine") + cmd.Flags().StringSlice("labels", nil, "labels to replace for the machine") + cmd.Flags().StringSlice("add-labels", nil, "labels to add to the machine") + cmd.Flags().StringSlice("remove-labels", nil, "labels to remove to the machine") + cmd.Flags().StringP("ssh-public-key", "i", "", + `SSH public key for access via ssh and console. [optional] +Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". +If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order.`) + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + }, + UpdateRequestFromCLI: func(args []string) (*apiv2.MachineServiceUpdateRequest, error) { + return helpers.MachineUpdateRequestFromCLI(c, args) + }, + DescribeCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project of the machine") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + }, + DeleteCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project of the machine") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.Project)) + }, + ValidArgsFn: c.Completion.Machine, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *machine) Create(rq *apiv2.MachineServiceCreateRequest) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Machine().Create(ctx, rq) + if err != nil { + if errorutil.IsConflict(err) { + return nil, genericcli.AlreadyExistsError() + } + + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) Delete(id string) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.MachineServiceDeleteRequest{ + Uuid: id, + Project: c.c.GetProject(), + } + + if viper.IsSet("file") { + var err error + req.Uuid, req.Project, err = helpers.DecodeProject(id) + if err != nil { + return nil, err + } + } + + resp, err := c.c.Client.Apiv2().Machine().Delete(ctx, req) + if err != nil { + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) Get(id string) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Machine().Get(ctx, &apiv2.MachineServiceGetRequest{ + Project: c.c.GetProject(), + Uuid: id, + }) + if err != nil { + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) List() ([]*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + var allocation *apiv2.MachineAllocationQuery + + if viper.IsSet("hostname") || viper.IsSet("image") { + allocation = &apiv2.MachineAllocationQuery{ + Hostname: pointer.PointerOrNil(viper.GetString("hostname")), + Image: pointer.PointerOrNil(viper.GetString("image")), + } + } + + resp, err := c.c.Client.Apiv2().Machine().List(ctx, &apiv2.MachineServiceListRequest{ + Project: c.c.GetProject(), + Query: &apiv2.MachineQuery{ + Uuid: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Size: pointer.PointerOrNil(viper.GetString("size")), + Allocation: allocation, + }, + }) + if err != nil { + return nil, err + } + + return resp.Machines, nil +} + +func (c *machine) Update(rq *apiv2.MachineServiceUpdateRequest) (*apiv2.Machine, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Machine().Update(ctx, rq) + if err != nil { + return nil, err + } + + return resp.Machine, nil +} + +func (c *machine) Convert(r *apiv2.Machine) (string, *apiv2.MachineServiceCreateRequest, *apiv2.MachineServiceUpdateRequest, error) { + update, err := helpers.MachineResponseToUpdate(r) + if err != nil { + return "", nil, nil, err + } + + create, err := helpers.MachineResponseToCreate(r) + if err != nil { + return "", nil, nil, err + } + + return helpers.EncodeProject(r.Uuid, r.Allocation.Project), create, update, err +} diff --git a/cmd/completion/image.go b/cmd/completion/image.go index 3c2021b..08d818f 100644 --- a/cmd/completion/image.go +++ b/cmd/completion/image.go @@ -6,6 +6,21 @@ import ( "github.com/spf13/cobra" ) +func (c *Completion) Image(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.Client.Apiv2().Image().List(cmd.Context(), &apiv2.ImageServiceListRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + + for _, img := range resp.Images { + names = append(names, img.Id+"\t"+*img.Name) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} + func (c *Completion) ImageFeatures(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var names []string diff --git a/cmd/completion/ip.go b/cmd/completion/ip.go index 8071fc6..0ae829d 100644 --- a/cmd/completion/ip.go +++ b/cmd/completion/ip.go @@ -20,6 +20,7 @@ func (c *Completion) Ip(cmd *cobra.Command, args []string, toComplete string) ([ } return names, cobra.ShellCompDirectiveNoFileComp } + func (c *Completion) AddressFamily(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var afs []string for _, af := range []apiv2.IPAddressFamily{ @@ -34,6 +35,7 @@ func (c *Completion) AddressFamily(cmd *cobra.Command, args []string, toComplete return afs, cobra.ShellCompDirectiveNoFileComp } + func (c *Completion) IPType(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var afs []string for _, af := range []apiv2.IPType{ diff --git a/cmd/completion/machine.go b/cmd/completion/machine.go index 36f1b06..6f930f0 100644 --- a/cmd/completion/machine.go +++ b/cmd/completion/machine.go @@ -2,24 +2,79 @@ package completion import ( adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/spf13/cobra" ) func (c *Completion) Machine(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - req := &adminv2.MachineServiceListRequest{} - resp, err := c.Client.Adminv2().Machine().List(cmd.Context(), req) + resp, err := c.Client.Apiv2().Machine().List(cmd.Context(), &apiv2.MachineServiceListRequest{ + // FIXME this only works for end users, not admin because they have no project set + Project: c.Proj, + }) if err != nil { return nil, cobra.ShellCompDirectiveError } + + var names []string + + for _, m := range resp.Machines { + name := m.Uuid + if m.Allocation != nil { + name = m.Uuid + "\t" + m.Allocation.Hostname + } + names = append(names, name) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) AdminMachine(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.Client.Adminv2().Machine().List(cmd.Context(), &adminv2.MachineServiceListRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, m := range resp.Machines { - var hostname string + name := m.Uuid if m.Allocation != nil { - hostname = m.Allocation.Hostname - names = append(names, m.Uuid+"\t"+hostname) - } else { - names = append(names, m.Uuid) + name = m.Uuid + "\t" + m.Allocation.Hostname } + names = append(names, name) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) Firewall(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.Client.Adminv2().Machine().List(cmd.Context(), &adminv2.MachineServiceListRequest{ + Query: &apiv2.MachineQuery{ + Allocation: &apiv2.MachineAllocationQuery{ + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL.Enum(), + }, + }, + }) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + + for _, m := range resp.Machines { + name := m.Uuid + "\t" + m.Allocation.Hostname + names = append(names, name) } + + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) BMCCommands(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var names []string + + for _, name := range apiv2.MachineBMCCommand_name { + names = append(names, name) + } + return names, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/config/config.go b/cmd/config/config.go index a5b9017..fdc692c 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -26,7 +26,7 @@ const ( ) type Config struct { - Fs afero.Fs + Fs *afero.Afero In io.Reader Out io.Writer PromptOut io.Writer diff --git a/cmd/root.go b/cmd/root.go index ae6ea2d..821a391 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,9 @@ import ( func Execute() { cfg := &config.Config{ - Fs: afero.NewOsFs(), + Fs: &afero.Afero{ + Fs: afero.NewOsFs(), + }, Out: os.Stdout, PromptOut: os.Stdout, In: os.Stdin, diff --git a/cmd/sorters/machine.go b/cmd/sorters/machine.go new file mode 100644 index 0000000..064ed1d --- /dev/null +++ b/cmd/sorters/machine.go @@ -0,0 +1,37 @@ +package sorters + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/multisort" + "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func MachineSorter() *multisort.Sorter[*apiv2.Machine] { + return multisort.New(multisort.FieldMap[*apiv2.Machine]{ + "partition": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Partition).Id, pointer.SafeDeref(b.Partition).Id, descending) + }, + "size": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Size).Id, pointer.SafeDeref(b.Size).Id, descending) + }, + "uuid": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(a.Uuid, b.Uuid, descending) + }, + "image": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(pointer.SafeDeref(a.Allocation).Image).Id, pointer.SafeDeref(pointer.SafeDeref(b.Allocation).Image).Id, descending) + }, + "rack": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(a.Rack, b.Rack, descending) + }, + "project": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Allocation).Project, pointer.SafeDeref(b.Allocation).Project, descending) + }, + "age": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare( + new(pointer.SafeDeref(pointer.SafeDeref(pointer.SafeDeref(a.Allocation).Meta).CreatedAt)).AsTime().Unix(), + new(pointer.SafeDeref(pointer.SafeDeref(pointer.SafeDeref(b.Allocation).Meta).CreatedAt)).AsTime().Unix(), + descending, + ) + }, + }, multisort.Keys{{ID: "partition"}, {ID: "size"}, {ID: "project"}, {ID: "uuid"}}) +} diff --git a/cmd/tableprinters/common.go b/cmd/tableprinters/common.go index 8b31e78..0de0a57 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -18,15 +18,8 @@ const ( nbr = " " halfpie = "◒" threequarterpie = "◕" - - ambulance = "🚑" - exclamation = "❗" - bark = "🚧" - loop = "⭕" - lock = "🔒" - question = "❓" - skull = "💀" - vpn = "🛡" + poweron = "⏻" + powersleep = "⏾" ) type TablePrinter struct { @@ -67,6 +60,14 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case []*apiv2.Network: return t.NetworkTable(d, wide) + case *apiv2.Machine: + return t.MachineTable(pointer.WrapInSlice(d), wide) + case []*apiv2.Machine: + return t.MachineTable(d, wide) + + case map[string]*apiv2.MachineBMCReport: + return t.MachineBMCTable(d, wide) + case *apiv2.IP: return t.IPTable(pointer.WrapInSlice(d), wide) case []*apiv2.IP: diff --git a/cmd/tableprinters/machine.go b/cmd/tableprinters/machine.go new file mode 100644 index 0000000..92f436d --- /dev/null +++ b/cmd/tableprinters/machine.go @@ -0,0 +1,273 @@ +package tableprinters + +import ( + "fmt" + "strings" + "time" + + "github.com/fatih/color" + "github.com/metal-stack/api/go/enum" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/helpers" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func (t *TablePrinter) MachineTable(data []*apiv2.Machine, wide bool) ([]string, [][]string, error) { + + var ( + rows [][]string + header = []string{"ID", "", "Last Event", "When", "Age", "Hostname", "Project", "Size", "Image", "Partition", "Rack"} + ) + + if wide { + header = []string{"ID", "Last Event", "When", "Age", "Description", "Name", "Hostname", "Project", "Ips", "Size", "Image", "Partition", "Rack", "Started", "Tags", "State"} + } + + for _, machine := range data { + machineID := machine.Uuid + + if machine.Status != nil && machine.Status.LedState != nil && machine.Status.LedState.Value == "LED-ON" { + blue := color.New(color.FgBlue).SprintFunc() + machineID = blue(machineID) + } + + var ( + alloc = pointer.SafeDeref(machine.Allocation) + sizeID = pointer.SafeDeref(machine.Size).Id + partitionID = pointer.SafeDeref(machine.Partition).Id + project = alloc.Project + name = alloc.Name + desc = alloc.Description + hostname = alloc.Hostname + image = pointer.SafeDeref(pointer.SafeDeref(alloc.Image).Name) + rack = machine.Rack + truncatedHostname = genericcli.TruncateEnd(hostname, 30) + + nwIPs []string + ) + + for _, nw := range alloc.Networks { + nwIPs = append(nwIPs, nw.Ips...) + } + + var ( + ips = strings.Join(nwIPs, "\n") + started = "" + age = "" + tags = "" + reserved = "" + lastEvent = "" + when = "" + ) + + if alloc.Meta != nil && alloc.Meta.CreatedAt != nil && !alloc.Meta.CreatedAt.AsTime().IsZero() { + started = alloc.Meta.CreatedAt.AsTime().Format(time.RFC3339) + age = humanizeDuration(time.Since(alloc.Meta.CreatedAt.AsTime())) + } + + if machine.Meta.Labels != nil && len(machine.Meta.Labels.Labels) > 0 { + var labels []string + for k, v := range machine.Meta.Labels.Labels { + labels = append(labels, k+"="+v) + } + tags = strings.Join(labels, ",") + } + + if machine.Status.Condition != nil { + stateString, err := enum.GetStringValue(machine.Status.Condition.State) + if err != nil { + return nil, nil, err + } + + reserved = *stateString + if machine.Status.Condition.Description != "" { + reserved += ":" + machine.Status.Condition.Description + } + } + + if len(machine.RecentProvisioningEvents.Events) > 0 { + since := time.Since(machine.RecentProvisioningEvents.LastEventTime.AsTime()) + when = humanizeDuration(since) + lastEventString, err := enum.GetStringValue(machine.RecentProvisioningEvents.Events[0].Event) + if err != nil { + return nil, nil, err + } + lastEvent = *lastEventString + } + + emojis := t.getMachineStatusEmojis(machine) + + if wide { + rows = append(rows, []string{machineID, lastEvent, when, age, desc, name, hostname, project, ips, sizeID, image, partitionID, rack, started, tags, reserved}) + } else { + rows = append(rows, []string{machineID, emojis, lastEvent, when, age, truncatedHostname, project, sizeID, image, partitionID, rack}) + } + } + + t.t.DisableAutoWrap(false) + + return header, rows, nil +} + +func (t *TablePrinter) MachineBMCTable(data map[string]*apiv2.MachineBMCReport, wide bool) ([]string, [][]string, error) { + var ( + rows [][]string + header = []string{"ID", "Power", "IP", "Mac", "Board Part Number", "Bios", "BMC", "Size", "Partition", "Rack", "Updated"} + ) + + if wide { + header = []string{"ID", "Power", "IP", "Mac", "Board Part Number", "Chassis Serial", "Product Serial", "Bios Version", "BMC Version", "Size", "Partition", "Rack", "Updated"} + } + + for machineID, report := range data { + // partition := pointer.SafeDeref(machine.).ID + // size := pointer.SafeDeref(pointer.SafeDeref(machine.Size).ID) + + if report.LedState != nil && report.LedState.Value == "LED-ON" { + blue := color.New(color.FgBlue).SprintFunc() + machineID = blue(machineID) + } + + var ( + // FIXME these are not provided by machineBMCReport + // FIXME Events are also not provided. + size = "" + partition = "" + rack = "" + + ipAddress = "" + mac = "" + bpn = "" + cs = "" + ps = "" + bmcVersion = "" + bmc = report.Bmc + fru = report.Fru + lastUpdated = "never" + bios = report.Bios + biosVersion = "" + ) + if fru != nil { + bpn = pointer.SafeDeref(fru.BoardPartNumber) + cs = pointer.SafeDeref(fru.ChassisPartSerial) + ps = pointer.SafeDeref(fru.ProductSerial) + } + + if bmc != nil { + ipAddress = bmc.Address + mac = bmc.Mac + bmcVersion = bmc.Version + + } + power, powerText := extractPowerState(report) + + if report.UpdatedAt != nil && !report.UpdatedAt.AsTime().IsZero() { + lastUpdated = fmt.Sprintf("%s ago", humanizeDuration(time.Since(report.UpdatedAt.AsTime()))) + } + + if bios != nil { + biosVersion = bios.Version + } + + if wide { + rows = append(rows, []string{machineID, powerText, ipAddress, mac, bpn, cs, ps, biosVersion, bmcVersion, size, partition, rack, lastUpdated}) + } else { + rows = append(rows, []string{machineID, power, ipAddress, mac, bpn, biosVersion, bmcVersion, size, partition, rack, lastUpdated}) + } + + } + + t.t.DisableAutoWrap(false) + + return header, rows, nil +} + +func extractPowerState(bmc *apiv2.MachineBMCReport) (short, wide string) { + if bmc == nil || bmc.Bmc == nil { + return color.WhiteString(poweron), wide + } + + state := bmc.Bmc.PowerState + switch state { + case "ON": + short = color.GreenString(poweron) + case "OFF": + short = color.GreenString(powersleep) + default: + short = color.WhiteString(poweron) + } + + wide = state + for _, ps := range bmc.PowerSupplies { + if ps.Health != "OK" { + short = color.RedString(poweron) + wide = wide + nbr + "Power Supply" + nbr + ps.Health + } + if ps.State != "Enabled" { + short = color.RedString(powersleep) + wide = wide + nbr + ps.State + } + } + + if bmc.PowerMetric != nil { + short = fmt.Sprintf("%s"+nbr+"%.0fW", short, bmc.PowerMetric.AverageConsumedWatts) + wide = fmt.Sprintf("%s %.0fW", wide, bmc.PowerMetric.AverageConsumedWatts) + } + + return short, wide +} + +func (t *TablePrinter) getMachineStatusEmojis(m *apiv2.Machine) string { + if m == nil { + return "" + } + + var ( + emojis []string + ) + + if status := m.Status; status != nil { + switch status.Liveliness { + case apiv2.MachineLiveliness_MACHINE_LIVELINESS_ALIVE: + // noop + case apiv2.MachineLiveliness_MACHINE_LIVELINESS_DEAD: + emojis = append(emojis, helpers.Skull) + default: + emojis = append(emojis, helpers.Question) + } + + if status.Condition != nil { + switch status.Condition.State { + case apiv2.MachineState_MACHINE_STATE_LOCKED: + emojis = append(emojis, helpers.Lock) + case apiv2.MachineState_MACHINE_STATE_TAINTED: + emojis = append(emojis, helpers.Bark) + default: + // noop + } + } + } + + if events := m.RecentProvisioningEvents; events != nil { + switch events.State { + case apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_FAILED_RECLAIM: + emojis = append(emojis, helpers.Ambulance) + case apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_CRASHLOOP: + emojis = append(emojis, helpers.Loop) + default: + // noop + + } + + if time.Since(events.LastErrorEvent.Time.AsTime()) < t.lastEventErrorThreshold { + emojis = append(emojis, helpers.Exclamation) + } + } + + if m.Allocation != nil && m.Allocation.Vpn != nil && m.Allocation.Vpn.Connected { + emojis = append(emojis, helpers.VPN) + } + + return strings.Join(emojis, nbr) +} diff --git a/cmd/tableprinters/switch.go b/cmd/tableprinters/switch.go index f898caa..2a0cc47 100644 --- a/cmd/tableprinters/switch.go +++ b/cmd/tableprinters/switch.go @@ -313,60 +313,6 @@ func (t *TablePrinter) SwitchDetailTable(switches []SwitchDetail) ([]string, [][ return header, rows, nil } -func (t *TablePrinter) getMachineStatusEmojis(m *apiv2.Machine) string { - if m == nil { - return "" - } - - var ( - emojis []string - ) - - if status := m.Status; status != nil { - switch status.Liveliness { - case apiv2.MachineLiveliness_MACHINE_LIVELINESS_ALIVE: - // noop - case apiv2.MachineLiveliness_MACHINE_LIVELINESS_DEAD: - emojis = append(emojis, skull) - default: - emojis = append(emojis, question) - } - - if status.Condition != nil { - switch status.Condition.State { - case apiv2.MachineState_MACHINE_STATE_LOCKED: - emojis = append(emojis, lock) - case apiv2.MachineState_MACHINE_STATE_TAINTED: - emojis = append(emojis, bark) - default: - // noop - } - } - } - - if events := m.RecentProvisioningEvents; events != nil { - switch events.State { - case apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_FAILED_RECLAIM: - emojis = append(emojis, ambulance) - case apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_CRASHLOOP: - emojis = append(emojis, loop) - default: - // noop - - } - - if time.Since(events.LastErrorEvent.Time.AsTime()) < t.lastEventErrorThreshold { - emojis = append(emojis, exclamation) - } - } - - if m.Allocation != nil && m.Allocation.Vpn != nil && m.Allocation.Vpn.Connected { - emojis = append(emojis, vpn) - } - - return strings.Join(emojis, nbr) -} - func filterColumns(filter *apiv2.BGPFilter, i int) []string { var ( vni string diff --git a/docs/admin/metalctlv2_admin.md b/docs/admin/metalctlv2_admin.md index 9ed2106..d32ad0b 100644 --- a/docs/admin/metalctlv2_admin.md +++ b/docs/admin/metalctlv2_admin.md @@ -32,6 +32,7 @@ these commands utilize the admin api, which can only be accessed by metal-stack * [metalctlv2 admin component](metalctlv2_admin_component.md) - manage component entities * [metalctlv2 admin image](metalctlv2_admin_image.md) - manage image entities * [metalctlv2 admin ip](metalctlv2_admin_ip.md) - manage ip entities +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities * [metalctlv2 admin network](metalctlv2_admin_network.md) - manage network entities * [metalctlv2 admin partition](metalctlv2_admin_partition.md) - manage partition entities * [metalctlv2 admin project](metalctlv2_admin_project.md) - manage project entities diff --git a/docs/admin/metalctlv2_admin_machine.md b/docs/admin/metalctlv2_admin_machine.md new file mode 100644 index 0000000..e465d3c --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine.md @@ -0,0 +1,43 @@ +## metalctlv2 admin machine + +manage machine entities + +### Synopsis + +manage machines + +### Options + +``` + -h, --help help for machine +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin](metalctlv2_admin.md) - admin commands +* [metalctlv2 admin machine apply](metalctlv2_admin_machine_apply.md) - applies one or more machines from a given file +* [metalctlv2 admin machine bmc-command](metalctlv2_admin_machine_bmc-command.md) - send a command to the bmc of a machine +* [metalctlv2 admin machine console](metalctlv2_admin_machine_console.md) - establishes a connection to the serial console of a machine. for authentication at the metal-console it uses the token such that no machine ssh key is required for access (unlike the corresponding user API command). +* [metalctlv2 admin machine create](metalctlv2_admin_machine_create.md) - creates the machine +* [metalctlv2 admin machine delete](metalctlv2_admin_machine_delete.md) - deletes the machine +* [metalctlv2 admin machine describe](metalctlv2_admin_machine_describe.md) - describes the machine +* [metalctlv2 admin machine edit](metalctlv2_admin_machine_edit.md) - edit the machine through an editor and update +* [metalctlv2 admin machine list](metalctlv2_admin_machine_list.md) - list all machines +* [metalctlv2 admin machine lock](metalctlv2_admin_machine_lock.md) - lock or unlock a machine, e.g. machine cannot be used +* [metalctlv2 admin machine ssh](metalctlv2_admin_machine_ssh.md) - SSH to a firewall +* [metalctlv2 admin machine taint](metalctlv2_admin_machine_taint.md) - taint or untaint a machine, e.g. machine will not be automatically selected on machine create, only admins can create them +* [metalctlv2 admin machine update](metalctlv2_admin_machine_update.md) - updates the machine + diff --git a/docs/admin/metalctlv2_admin_machine_apply.md b/docs/admin/metalctlv2_admin_machine_apply.md new file mode 100644 index 0000000..a60524b --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_apply.md @@ -0,0 +1,46 @@ +## metalctlv2 admin machine apply + +applies one or more machines from a given file + +``` +metalctlv2 admin machine apply [flags] +``` + +### Options + +``` + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine apply -f - + $ # or via file + $ metalctlv2 machine apply -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for apply + --skip-security-prompts skips security prompt for bulk operations + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_bmc-command.md b/docs/admin/metalctlv2_admin_machine_bmc-command.md new file mode 100644 index 0000000..66a1d2d --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_bmc-command.md @@ -0,0 +1,32 @@ +## metalctlv2 admin machine bmc-command + +send a command to the bmc of a machine + +``` +metalctlv2 admin machine bmc-command [flags] +``` + +### Options + +``` + --command string the actual command to send to the machine + -h, --help help for bmc-command +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_console.md b/docs/admin/metalctlv2_admin_machine_console.md new file mode 100644 index 0000000..95c011a --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_console.md @@ -0,0 +1,33 @@ +## metalctlv2 admin machine console + +establishes a connection to the serial console of a machine. for authentication at the metal-console it uses the token such that no machine ssh key is required for access (unlike the corresponding user API command). + +``` +metalctlv2 admin machine console [flags] +``` + +### Options + +``` + -h, --help help for console + --ipmi if set to true, the serial console will be opened using ipmitool (requires ipmitool to be present) + --metal-console-port int port open on our control-plane to connect via ssh to get machine console access (default 5222) +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_create.md b/docs/admin/metalctlv2_admin_machine_create.md new file mode 100644 index 0000000..8145e72 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_create.md @@ -0,0 +1,109 @@ +## metalctlv2 admin machine create + +creates the machine + +``` +metalctlv2 admin machine create [flags] +``` + +### Examples + +``` +machine create can be done in two different ways: + +- default with automatic allocation: + + metalctl machine create \ + --hostname worker01 \ + --name worker \ + --image ubuntu-18.04 \ # query available with: metalctl image list + --size t1-small-x86 \ # query available with: metalctl size list + --partition test \ # query available with: metalctl partition list + --project cluster01 \ + --sshpublickey "@~/.ssh/id_rsa.pub" + +- for metal administration with reserved machines: + + reserve a machine you want to allocate: + + metalctl machine reserve 00000000-0000-0000-0000-0cc47ae54694 --description "blocked for maintenance" + + allocate this machine: + + metalctl machine create \ + --hostname worker01 \ + --name worker \ + --image ubuntu-18.04 \ # query available with: metalctl image list + --project cluster01 \ + --sshpublickey "@~/.ssh/id_rsa.pub" \ + --id 00000000-0000-0000-0000-0cc47ae54694 + +after you do not want to use this machine exclusive, remove the reservation: + +metalctl machine reserve 00000000-0000-0000-0000-0cc47ae54694 --remove + +Once created the machine installation can not be modified anymore. + +``` + +### Options + +``` + --allocation-type string allocation type, can be either machine|firewall (default "machine") + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + --description string Description of the machine to create. [optional] + --dns-servers strings dns servers to add to the machine or firewall. [optional] + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine create -f - + $ # or via file + $ metalctlv2 machine create -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + --filesystem-layout string Filesystemlayout to use during machine installation. [optional] + -h, --help help for create + --hostname string Hostname of the machine. [required] + --id string id of the machine to create. [optional] + --image string OS Image to install. [required] + --labels strings labels to add to the machine, use it like: --labels "a=b" or --labels "a=". + --name string Name of the machine. [optional] + --networks strings Adds a network. Usage: [--networks NETWORK[:ip[;ip]][,NETWORK[:ip[;ip]]... + NETWORK specifies the name or id of an existing network. + IPs can be added per network colon separated, these ips must be already allocated upfront. If no ip(s) are specified per network, one ip per network is allocated. + + --ntp-servers strings ntp servers to add to the machine or firewall. [optional] + --partition string partition/datacenter where the machine is created. [required, except for reserved machines] + --placement-tags strings placement tags used for rack spreading + -p, --project string Project where the machine should belong to. [required] + --size string Size of the machine. [required, except for reserved machines] + --skip-security-prompts skips security prompt for bulk operations + -i, --ssh-public-key string SSH public key for access via ssh and console. [optional] + Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". + If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order. + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations + --userdata string cloud-init.io compatible userdata. [optional] + Can be either the userdata as string, or pointing to the userdata file to use e.g.: "@/tmp/userdata.cfg". +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_delete.md b/docs/admin/metalctlv2_admin_machine_delete.md new file mode 100644 index 0000000..d1befd2 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_delete.md @@ -0,0 +1,46 @@ +## metalctlv2 admin machine delete + +deletes the machine + +``` +metalctlv2 admin machine delete [flags] +``` + +### Options + +``` + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine delete -f - + $ # or via file + $ metalctlv2 machine delete -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for delete + --skip-security-prompts skips security prompt for bulk operations + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_describe.md b/docs/admin/metalctlv2_admin_machine_describe.md new file mode 100644 index 0000000..2f5894b --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_describe.md @@ -0,0 +1,32 @@ +## metalctlv2 admin machine describe + +describes the machine + +``` +metalctlv2 admin machine describe [flags] +``` + +### Options + +``` + -h, --help help for describe + -p, --project string project of the machine +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_edit.md b/docs/admin/metalctlv2_admin_machine_edit.md new file mode 100644 index 0000000..a3ae867 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_edit.md @@ -0,0 +1,31 @@ +## metalctlv2 admin machine edit + +edit the machine through an editor and update + +``` +metalctlv2 admin machine edit [flags] +``` + +### Options + +``` + -h, --help help for edit +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_list.md b/docs/admin/metalctlv2_admin_machine_list.md new file mode 100644 index 0000000..a1463d7 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_list.md @@ -0,0 +1,55 @@ +## metalctlv2 admin machine list + +list all machines + +### Synopsis + +list all machines + +Meaning of the emojis: + +🚧 Machine is reserved. Reserved machines are not considered for random allocation until the reservation flag is removed. +🔒 Machine is locked. Locked machines can not be deleted until the lock is removed. +💀 Machine is dead. The metal-api does not receive any events from this machine. +❗ Machine has a last event error. The machine has recently encountered an error during the provisioning lifecycle. +❓ Machine is in unknown condition. The metal-api does not receive phoned home events anymore or has never booted successfully. +⭕ Machine is in a provisioning crash loop. Flag can be reset through an API-triggered reboot or when the machine reaches the phoned home state. +🚑 Machine reclaim has failed. The machine was deleted but it is not going back into the available machine pool. +🛡 Machine is connected to our VPN, ssh access only possible via this VPN. + + +``` +metalctlv2 admin machine list [flags] +``` + +### Options + +``` + -h, --help help for list + --hostname string hostname from machines which should be listed + --id string id of machine which should be listed + --image string image + --name string name from machines which should be listed + --partition string partition from where machines should be listed + -p, --project string project from where machines should be listed + --size string size from machines which should be listed + --sort-by strings sort by (comma separated) column(s), sort direction can be changed by appending :asc or :desc behind the column identifier. possible values: age|image|partition|project|rack|size|uuid +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_lock.md b/docs/admin/metalctlv2_admin_machine_lock.md new file mode 100644 index 0000000..3163051 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_lock.md @@ -0,0 +1,33 @@ +## metalctlv2 admin machine lock + +lock or unlock a machine, e.g. machine cannot be used + +``` +metalctlv2 admin machine lock [flags] +``` + +### Options + +``` + --description string description of why the machine was locked + -h, --help help for lock + --remove if set to true, machine will be unlocked +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_ssh.md b/docs/admin/metalctlv2_admin_machine_ssh.md new file mode 100644 index 0000000..621ab65 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_ssh.md @@ -0,0 +1,37 @@ +## metalctlv2 admin machine ssh + +SSH to a firewall + +### Synopsis + +SSH to a firewall via VPN. + +``` +metalctlv2 admin machine ssh [flags] +``` + +### Options + +``` + -h, --help help for ssh + -i, --identity string specify identity file to SSH to the firewall like: -i path/to/id_rsa (default "~/.ssh/id_rsa") + --reason string the reason why to connect to the firewall through SSH +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_taint.md b/docs/admin/metalctlv2_admin_machine_taint.md new file mode 100644 index 0000000..e0290f4 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_taint.md @@ -0,0 +1,33 @@ +## metalctlv2 admin machine taint + +taint or untaint a machine, e.g. machine will not be automatically selected on machine create, only admins can create them + +``` +metalctlv2 admin machine taint [flags] +``` + +### Options + +``` + --description string description of why the machine was tainted + -h, --help help for taint + --remove if set to true, machine will be untainted +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/admin/metalctlv2_admin_machine_update.md b/docs/admin/metalctlv2_admin_machine_update.md new file mode 100644 index 0000000..2fa6d58 --- /dev/null +++ b/docs/admin/metalctlv2_admin_machine_update.md @@ -0,0 +1,54 @@ +## metalctlv2 admin machine update + +updates the machine + +``` +metalctlv2 admin machine update [flags] +``` + +### Options + +``` + --add-labels strings labels to add to the machine + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + --description string description of the machine + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine update -f - + $ # or via file + $ metalctlv2 machine update -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for update + --labels strings labels to replace for the machine + -p, --project string project from where machines should be listed + --remove-labels strings labels to remove to the machine + --skip-security-prompts skips security prompt for bulk operations + -i, --ssh-public-key string SSH public key for access via ssh and console. [optional] + Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". + If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order. + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 admin machine](metalctlv2_admin_machine.md) - manage machine entities + diff --git a/docs/metalctlv2.md b/docs/metalctlv2.md index b1579d5..a4524aa 100644 --- a/docs/metalctlv2.md +++ b/docs/metalctlv2.md @@ -27,6 +27,7 @@ cli for managing entities in metal-stack * [metalctlv2 ip](metalctlv2_ip.md) - manage ip entities * [metalctlv2 login](metalctlv2_login.md) - login * [metalctlv2 logout](metalctlv2_logout.md) - logout +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities * [metalctlv2 markdown](metalctlv2_markdown.md) - create markdown documentation * [metalctlv2 network](metalctlv2_network.md) - manage network entities * [metalctlv2 partition](metalctlv2_partition.md) - manage partition entities diff --git a/docs/metalctlv2_ip_update.md b/docs/metalctlv2_ip_update.md index 2c402d7..6b437e5 100644 --- a/docs/metalctlv2_ip_update.md +++ b/docs/metalctlv2_ip_update.md @@ -9,28 +9,29 @@ metalctlv2 ip update [flags] ### Options ``` - --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. - --description string description of the ip - -f, --file string filename of the create or update request in yaml format, or - for stdin. - - Example: - $ metalctlv2 ip describe ip-1 -o yaml > ip.yaml - $ vi ip.yaml - $ # either via stdin - $ cat ip.yaml | metalctlv2 ip update -f - - $ # or via file - $ metalctlv2 ip update -f ip.yaml - - the file can also contain multiple documents and perform a bulk operation. - - -h, --help help for update - --labels stringArray adds (or edits) the volume labels in the form of = - --name string name of the ip - -p, --project string project of the ip - --remove-labels stringArray removes the volume labels with the given key - --skip-security-prompts skips security prompt for bulk operations - --static make this ip static - --timestamps when used with --file (bulk operation): prints timestamps in-between the operations + --add-labels strings labels to add to the ip + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + --description string description of the ip + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 ip describe ip-1 -o yaml > ip.yaml + $ vi ip.yaml + $ # either via stdin + $ cat ip.yaml | metalctlv2 ip update -f - + $ # or via file + $ metalctlv2 ip update -f ip.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for update + --labels strings labels to replace for the ip + --name string name of the ip + -p, --project string project of the ip + --remove-labels strings labels to remove to the ip + --skip-security-prompts skips security prompt for bulk operations + --static make this ip static + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations ``` ### Options inherited from parent commands diff --git a/docs/metalctlv2_machine.md b/docs/metalctlv2_machine.md new file mode 100644 index 0000000..8df768c --- /dev/null +++ b/docs/metalctlv2_machine.md @@ -0,0 +1,38 @@ +## metalctlv2 machine + +manage machine entities + +### Synopsis + +allocate a machine + +### Options + +``` + -h, --help help for machine +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2](metalctlv2.md) - cli for managing entities in metal-stack +* [metalctlv2 machine apply](metalctlv2_machine_apply.md) - applies one or more machines from a given file +* [metalctlv2 machine create](metalctlv2_machine_create.md) - creates the machine +* [metalctlv2 machine delete](metalctlv2_machine_delete.md) - deletes the machine +* [metalctlv2 machine describe](metalctlv2_machine_describe.md) - describes the machine +* [metalctlv2 machine edit](metalctlv2_machine_edit.md) - edit the machine through an editor and update +* [metalctlv2 machine list](metalctlv2_machine_list.md) - list all machines +* [metalctlv2 machine update](metalctlv2_machine_update.md) - updates the machine + diff --git a/docs/metalctlv2_machine_apply.md b/docs/metalctlv2_machine_apply.md new file mode 100644 index 0000000..da19bbe --- /dev/null +++ b/docs/metalctlv2_machine_apply.md @@ -0,0 +1,46 @@ +## metalctlv2 machine apply + +applies one or more machines from a given file + +``` +metalctlv2 machine apply [flags] +``` + +### Options + +``` + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine apply -f - + $ # or via file + $ metalctlv2 machine apply -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for apply + --skip-security-prompts skips security prompt for bulk operations + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/docs/metalctlv2_machine_create.md b/docs/metalctlv2_machine_create.md new file mode 100644 index 0000000..e789919 --- /dev/null +++ b/docs/metalctlv2_machine_create.md @@ -0,0 +1,68 @@ +## metalctlv2 machine create + +creates the machine + +``` +metalctlv2 machine create [flags] +``` + +### Options + +``` + --allocation-type string allocation type, can be either machine|firewall (default "machine") + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + --description string Description of the machine to create. [optional] + --dns-servers strings dns servers to add to the machine or firewall. [optional] + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine create -f - + $ # or via file + $ metalctlv2 machine create -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + --filesystem-layout string Filesystemlayout to use during machine installation. [optional] + -h, --help help for create + --hostname string Hostname of the machine. [required] + --image string OS Image to install. [required] + --labels strings labels to add to the machine, use it like: --labels "a=b" or --labels "a=". + --name string Name of the machine. [optional] + --networks strings Adds a network. Usage: [--networks NETWORK[:ip[;ip]][,NETWORK[:ip[;ip]]... + NETWORK specifies the name or id of an existing network. + IPs can be added per network colon separated, these ips must be already allocated upfront. If no ip(s) are specified per network, one ip per network is allocated. + + --ntp-servers strings ntp servers to add to the machine or firewall. [optional] + --partition string partition/datacenter where the machine is created. [required, except for reserved machines] + --placement-tags strings placement tags used for rack spreading + -p, --project string Project where the machine should belong to. [required] + --size string Size of the machine. [required, except for reserved machines] + --skip-security-prompts skips security prompt for bulk operations + -i, --ssh-public-key string SSH public key for access via ssh and console. [optional] + Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". + If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order. + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations + --userdata string cloud-init.io compatible userdata. [optional] + Can be either the userdata as string, or pointing to the userdata file to use e.g.: "@/tmp/userdata.cfg". +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/docs/metalctlv2_machine_delete.md b/docs/metalctlv2_machine_delete.md new file mode 100644 index 0000000..dcab7bd --- /dev/null +++ b/docs/metalctlv2_machine_delete.md @@ -0,0 +1,47 @@ +## metalctlv2 machine delete + +deletes the machine + +``` +metalctlv2 machine delete [flags] +``` + +### Options + +``` + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine delete -f - + $ # or via file + $ metalctlv2 machine delete -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for delete + -p, --project string project of the machine + --skip-security-prompts skips security prompt for bulk operations + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/docs/metalctlv2_machine_describe.md b/docs/metalctlv2_machine_describe.md new file mode 100644 index 0000000..3d9b27c --- /dev/null +++ b/docs/metalctlv2_machine_describe.md @@ -0,0 +1,32 @@ +## metalctlv2 machine describe + +describes the machine + +``` +metalctlv2 machine describe [flags] +``` + +### Options + +``` + -h, --help help for describe + -p, --project string project of the machine +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/docs/metalctlv2_machine_edit.md b/docs/metalctlv2_machine_edit.md new file mode 100644 index 0000000..10279d8 --- /dev/null +++ b/docs/metalctlv2_machine_edit.md @@ -0,0 +1,31 @@ +## metalctlv2 machine edit + +edit the machine through an editor and update + +``` +metalctlv2 machine edit [flags] +``` + +### Options + +``` + -h, --help help for edit +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/docs/metalctlv2_machine_list.md b/docs/metalctlv2_machine_list.md new file mode 100644 index 0000000..a71a75e --- /dev/null +++ b/docs/metalctlv2_machine_list.md @@ -0,0 +1,55 @@ +## metalctlv2 machine list + +list all machines + +### Synopsis + +list all machines + +Meaning of the emojis: + +🚧 Machine is reserved. Reserved machines are not considered for random allocation until the reservation flag is removed. +🔒 Machine is locked. Locked machines can not be deleted until the lock is removed. +💀 Machine is dead. The metal-api does not receive any events from this machine. +❗ Machine has a last event error. The machine has recently encountered an error during the provisioning lifecycle. +❓ Machine is in unknown condition. The metal-api does not receive phoned home events anymore or has never booted successfully. +⭕ Machine is in a provisioning crash loop. Flag can be reset through an API-triggered reboot or when the machine reaches the phoned home state. +🚑 Machine reclaim has failed. The machine was deleted but it is not going back into the available machine pool. +🛡 Machine is connected to our VPN, ssh access only possible via this VPN. + + +``` +metalctlv2 machine list [flags] +``` + +### Options + +``` + -h, --help help for list + --hostname string hostname from machines which should be listed + --image string image + --name string name from machines which should be listed + --partition string partition from where machines should be listed + -p, --project string project from where machines should be listed + --size string size from machines which should be listed + --sort-by strings sort by (comma separated) column(s), sort direction can be changed by appending :asc or :desc behind the column identifier. possible values: age|image|partition|project|rack|size|uuid + --uuid string allocation uuid of machine which should be listed +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/docs/metalctlv2_machine_update.md b/docs/metalctlv2_machine_update.md new file mode 100644 index 0000000..bd72d06 --- /dev/null +++ b/docs/metalctlv2_machine_update.md @@ -0,0 +1,54 @@ +## metalctlv2 machine update + +updates the machine + +``` +metalctlv2 machine update [flags] +``` + +### Options + +``` + --add-labels strings labels to add to the machine + --bulk-output when used with --file (bulk operation): prints results at the end as a list. default is printing results intermediately during the operation, which causes single entities to be printed in a row. + --description string description of the machine + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 machine describe machine-1 -o yaml > machine.yaml + $ vi machine.yaml + $ # either via stdin + $ cat machine.yaml | metalctlv2 machine update -f - + $ # or via file + $ metalctlv2 machine update -f machine.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for update + --labels strings labels to replace for the machine + -p, --project string project from where machines should be listed + --remove-labels strings labels to remove to the machine + --skip-security-prompts skips security prompt for bulk operations + -i, --ssh-public-key string SSH public key for access via ssh and console. [optional] + Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". + If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order. + --timestamps when used with --file (bulk operation): prints timestamps in-between the operations +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 machine](metalctlv2_machine.md) - manage machine entities + diff --git a/go.mod b/go.mod index ee0d712..eb2fabe 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/metal-stack/api v0.4.4 - github.com/metal-stack/metal-lib v0.26.2 + github.com/metal-stack/metal-lib v0.26.3 github.com/metal-stack/v v1.0.3 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 @@ -25,52 +25,91 @@ require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1 // indirect buf.build/go/protovalidate v1.3.0 // indirect buf.build/go/protoyaml v0.7.0 // indirect - cel.dev/expr v0.25.2 // indirect + cel.dev/expr v0.25.3 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/coder/websocket v1.8.15 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/creachadair/msync v0.10.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/gaissmai/bart v0.29.0 // indirect + github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect github.com/go-openapi/errors v0.22.8 // indirect github.com/go-openapi/strfmt v0.27.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.31.0 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jsimonetti/rtnetlink v1.4.2 // indirect github.com/klauspost/compress v1.19.2 // indirect github.com/klauspost/connect-compress/v2 v2.1.1 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.27 // indirect + github.com/mdlayher/netlink v1.11.2 // indirect + github.com/mdlayher/socket v0.6.1 // indirect github.com/minio/minlz v1.2.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.3.0 // indirect github.com/olekukonko/ll v0.1.8 // indirect github.com/olekukonko/tablewriter v1.1.4 // indirect github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/pires/go-proxyproto v0.15.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/safchain/ethtool v0.7.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f // indirect + github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368 // indirect + github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0 // indirect + github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/exp v0.0.0-20260812173653-3d80eb74bc5b // indirect + go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/net v0.58.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect golang.org/x/text v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + golang.zx2c4.com/wireguard/windows v1.0.1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 // indirect k8s.io/apimachinery v0.36.3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + tailscale.com v1.102.2 // indirect ) diff --git a/go.sum b/go.sum index 816f4a3..11b6c18 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,100 @@ +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= +9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1 h1:6nlcxMOui23ZRVAfJM451duu79P1npA5JRdZqMilrrQ= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= buf.build/go/protovalidate v1.3.0 h1:8ITcnZGkAHx6TyhZvro+iET/AyqU8gEWQJK2WsT62ms= buf.build/go/protovalidate v1.3.0/go.mod h1:82s5g+rFRj1CZPiLv6OTA31jBu2fpq7mLXHwa9mZfEs= buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38= buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0= -cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= -cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4= connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= +filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= +github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= +github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= +github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/ssm v1.45.0 h1:IOdss+igJDFdic9w3WKwxGCmHqUxydvIhJOm9LJ32Dk= +github.com/aws/aws-sdk-go-v2/service/ssm v1.45.0/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= +github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ= +github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= +github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= +github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creachadair/mds v0.30.2 h1:cOP0xkfYkk4mxqPl4SmjnrY+agrpgiEqsVygLnPvJto= +github.com/creachadair/mds v0.30.2/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI= +github.com/creachadair/msync v0.10.0 h1:2RlGs187RQN5tzyluKEkbkXq+LRK2KIR+An6FoB9x+M= +github.com/creachadair/msync v0.10.0/go.mod h1:J+4p7as+O7NWydXYGJNrigY67qj1F1GB0CcTWyV/5AE= +github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc= +github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ= +github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -33,6 +103,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gaissmai/bart v0.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI= +github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= +github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg= +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM= @@ -43,26 +123,58 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689 h1:0psnKZ+N2IP43/SZC8SKx6OpFJwLmQb9m9QyV9BC2f8= +github.com/go4org/hashtriemap v0.0.0-20251130024219-545ba229f689/go.mod h1:OGmRfY/9QEK2P5zCRtmqfbCF283xPkU2dvVA4MvbvpI= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo= +github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.31.0 h1:H0bhpFTqOvmHrBGrWKp7ZlhBm5Hh8PYUEXnwxT1LL7A= github.com/google/cel-go v0.31.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I= +github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk= +github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1 h1:L3pm9Kf2G6gJVYawz2SrI5QnV1wzHYbqmKnSHHXJAb8= +github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw= +github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= +github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90= +github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM= github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/connect-compress/v2 v2.1.1 h1:ycZNp4rWOZBodVE2Ls5AzK4aHkyK+GteEfzRZgKNs+c= github.com/klauspost/connect-compress/v2 v2.1.1/go.mod h1:9oilsPHJMzGKkjafSBk9J7iVo4mO+dw0G0KSdVpnlVE= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= +github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -73,14 +185,30 @@ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsRe github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= +github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= +github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= +github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= +github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/metal-stack/api v0.4.4 h1:NwJKCFHnbmsU7DjHLtBmpdPuPniXc2wDabHjdpXk3qA= github.com/metal-stack/api v0.4.4/go.mod h1:E7f2GkKNSr4vBxhQwWrr/Mjnf4ctPqCX2dpDtXBqkBk= -github.com/metal-stack/metal-lib v0.26.2 h1:ajPbC9wGe8LySw9J3JT03IGLZghvxsXSb7iceFp/FuQ= -github.com/metal-stack/metal-lib v0.26.2/go.mod h1:cNXjPBs8SFnjqfBobuSbm5mDk6E/jS8PVeIOrV/7POE= +github.com/metal-stack/metal-lib v0.26.3 h1:K5gLoD65m6p3l6qCPrfavIdvNdWfmF2QdXrvU2URaZs= +github.com/metal-stack/metal-lib v0.26.3/go.mod h1:cNXjPBs8SFnjqfBobuSbm5mDk6E/jS8PVeIOrV/7POE= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/minio/minlz v1.2.0 h1:6IOBuiHg04QxvbFfgFLT/9sMaO/UhL7S+ApW1mK8q5A= github.com/minio/minlz v1.2.0/go.mod h1:Ls9H7nlkASeCcdl5thjVD5Eraj6z+zGa7xtq57jIKD4= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -94,14 +222,26 @@ github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+m github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI= +github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is= +github.com/safchain/ethtool v0.7.0/go.mod h1:MenQKEjXdfkjD3mp2QdCk8B/hwvkrlOTm/FD4gTpFxQ= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -119,21 +259,84 @@ github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/studio-b12/gowebdav v0.13.0 h1:OcwSg6IQHOFNdYHn3bPOHwSE8looG8N56Y5xTT1asqQ= +github.com/studio-b12/gowebdav v0.13.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE= +github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28 h1:Azz5ILxxVsHN/KjIu3wkJPAmmtiijucZw4Ax5Ye8n+s= +github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/golang-x-crypto v0.0.0-20260720153645-2ba0bf7866ed h1:uyvHhX1FQada0vVk8CSHa4tJT96EEAkTypaYz8Tq5Nc= +github.com/tailscale/golang-x-crypto v0.0.0-20260720153645-2ba0bf7866ed/go.mod h1:NC3xRCu4UR+m4n6ix8b6oLLbHa820Y0StbOQEdWTDo0= +github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f h1:9hiVElpCmKzsBKQHkBqZ8LGzt82iLfM8egxr4sew+Ys= +github.com/tailscale/hujson v0.0.0-20260727124030-b80ff77dac4f/go.mod h1:8/zr1Tv0+cKpVtGCEB/7YfRXr2TszsMxMXLaT8YuBgU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= +github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= +github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368 h1:0tpDdAj9sSfSZg4gMwNTdqMP592sBrq2Sm0w6ipnh7k= +github.com/tailscale/web-client-prebuilt v0.0.0-20251127225136-f19339b67368/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= +github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0 h1:CnIEL2n7Xql6Ux1k+Vu5S5ubDHCT/kxFgkKCY8FjefU= +github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= +github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= +github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/exp v0.0.0-20260812173653-3d80eb74bc5b h1:3XR0v9KL97wjTmRb+6RqMylL4d7ZWM1sQYIITGDxtmg= -golang.org/x/exp v0.0.0-20260812173653-3d80eb74bc5b/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= +go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w= +golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= +golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8= +golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 h1:dWeMvEJ3JhYgqSCAHUZZJgMUyfniiiCvDc72x5EqJP0= google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754/go.mod h1:q/3oV3jAi5vwelxsVAprMBC8BcM2zmNe+IjRGd+9/ks= google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 h1:k5CJw9e5ONCcA/u0webKt092npXuY+KeGh3Q8NAVf0g= @@ -149,9 +352,19 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8 h1:Zy8IV/+FMLxy6j6p87vk/vQGKcdnbprwjTxc8UiUtsA= +gvisor.dev/gvisor v0.0.0-20260224225140-573d5e7127a8/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= +honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= +honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM= k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +tailscale.com v1.102.2 h1:K0TJMOFv0F9aJDSjM/C2uVtrwnLn+ek22c42x61FXeA= +tailscale.com v1.102.2/go.mod h1:ynxKzc9hDxwGLHORQE0qrTH1b8NBRrIsXcnfnug/3dg= diff --git a/pkg/helpers/emoji.go b/pkg/helpers/emoji.go new file mode 100644 index 0000000..63e9c56 --- /dev/null +++ b/pkg/helpers/emoji.go @@ -0,0 +1,27 @@ +package helpers + +const ( + Ambulance = "🚑" + Exclamation = "❗" + Bark = "🚧" + Loop = "⭕" + Lock = "🔒" + Question = "❓" + Skull = "💀" + VPN = "🛡" +) + +func EmojiHelpText() string { + return ` +Meaning of the emojis: + +🚧 Machine is reserved. Reserved machines are not considered for random allocation until the reservation flag is removed. +🔒 Machine is locked. Locked machines can not be deleted until the lock is removed. +💀 Machine is dead. The metal-api does not receive any events from this machine. +❗ Machine has a last event error. The machine has recently encountered an error during the provisioning lifecycle. +❓ Machine is in unknown condition. The metal-api does not receive phoned home events anymore or has never booted successfully. +⭕ Machine is in a provisioning crash loop. Flag can be reset through an API-triggered reboot or when the machine reaches the phoned home state. +🚑 Machine reclaim has failed. The machine was deleted but it is not going back into the available machine pool. +🛡 Machine is connected to our VPN, ssh access only possible via this VPN. +` +} diff --git a/pkg/helpers/machine.go b/pkg/helpers/machine.go new file mode 100644 index 0000000..1cd547d --- /dev/null +++ b/pkg/helpers/machine.go @@ -0,0 +1,359 @@ +package helpers + +import ( + "encoding/base64" + "fmt" + "net/netip" + "os" + osuser "os/user" + "path/filepath" + "strings" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/completion" + "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func MachineResponseToCreate(r *apiv2.Machine) (*apiv2.MachineServiceCreateRequest, error) { + if r.Allocation == nil { + return nil, fmt.Errorf("allocation is nil") + } + + var ( + networks []*apiv2.MachineAllocationNetwork + firewallSpec *apiv2.FirewallSpec + ) + + for _, nw := range r.Allocation.Networks { + networks = append(networks, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + if r.Allocation.AllocationType.Enum() == apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL.Enum() { + firewallSpec = &apiv2.FirewallSpec{ + FirewallRules: &apiv2.FirewallRules{ + Egress: r.Allocation.FirewallRules.Egress, + Ingress: r.Allocation.FirewallRules.Ingress, + }, + } + } + + return &apiv2.MachineServiceCreateRequest{ + Project: r.Allocation.Project, + Name: r.Allocation.Name, + Description: &r.Allocation.Description, + Hostname: &r.Allocation.Hostname, + Partition: new(pointer.SafeDeref(r.Partition).Id), + Size: new(pointer.SafeDeref(r.Size).Id), + Image: r.Allocation.Image.Id, + FilesystemLayout: pointer.PointerOrNil(r.Allocation.FilesystemLayout.Id), + SshPublicKeys: r.Allocation.SshPublicKeys, + Userdata: pointer.PointerOrNil(r.Allocation.Userdata), + Labels: pointer.SafeDeref(r.Meta).Labels, + Networks: networks, + DnsServers: r.Allocation.DnsServers, + NtpServers: r.Allocation.NtpServers, + AllocationType: r.Allocation.AllocationType, + FirewallSpec: firewallSpec, + // PlacementTags: r.Allocation.Plac, // TODO: should be stored in the allocation to see what was provided + }, nil +} + +func MachineResponseToUpdate(r *apiv2.Machine) (*apiv2.MachineServiceUpdateRequest, error) { + if r.Allocation == nil { + return nil, fmt.Errorf("allocation is nil") + } + + return &apiv2.MachineServiceUpdateRequest{ + Uuid: r.Uuid, + UpdateMeta: UpdateMetaFromMeta(r.Meta), + Labels: UpdateLabelsFromMeta(r.Meta), + Project: r.Allocation.Project, + Description: &r.Allocation.Description, + SshPublicKeys: r.Allocation.SshPublicKeys, + }, nil +} + +func MachineCreateRequestFromCLI(c *config.Config) (*apiv2.MachineServiceCreateRequest, error) { + var ( + keys []string + dnsServers []*apiv2.DNSServer + ntpServers []*apiv2.NTPServer + allocationType apiv2.MachineAllocationType + firewallSpec *apiv2.FirewallSpec + + sshPublicKeyArgument = viper.GetString("ssh-public-key") + dnsServersArgument = viper.GetStringSlice("dns-servers") + ntpServersArgument = viper.GetStringSlice("ntp-servers") + ) + + if strings.HasPrefix(sshPublicKeyArgument, "@") { + var err error + sshPublicKeyArgument, err = readFromFile(c.Fs, sshPublicKeyArgument[1:]) + if err != nil { + return nil, err + } + } + + if len(sshPublicKeyArgument) == 0 { + sshKey, err := SearchSSHKey() + if err != nil { + return nil, err + } + sshPublicKey := sshKey + ".pub" + sshPublicKeyArgument, err = readFromFile(c.Fs, sshPublicKey) + if err != nil { + return nil, err + } + } + + if sshPublicKeyArgument != "" { + keys = append(keys, sshPublicKeyArgument) + } + + userDataArgument := viper.GetString("userdata") + if strings.HasPrefix(userDataArgument, "@") { + var err error + userDataArgument, err = readFromFile(c.Fs, userDataArgument[1:]) + if err != nil { + return nil, err + } + } + if userDataArgument != "" { + userDataArgument = base64.StdEncoding.EncodeToString([]byte(userDataArgument)) + } + + possibleNetworks := viper.GetStringSlice("networks") + networks, err := parseNetworks(possibleNetworks) + if err != nil { + return nil, err + } + + for _, s := range dnsServersArgument { + dnsServers = append(dnsServers, &apiv2.DNSServer{Ip: s}) + } + + for _, s := range ntpServersArgument { + ntpServers = append(ntpServers, &apiv2.NTPServer{Address: s}) + } + + allocationType = apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE + if viper.GetString("allocation-type") == "firewall" { + allocationType = apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL + } + + labels, err := LabelsFromSlice(viper.GetStringSlice("labels")) + if err != nil { + return nil, err + } + + var filesystemlayout *string + if viper.IsSet("filesystem-layout") { + filesystemlayout = new(viper.GetString("filesystem-layout")) + } + var size *string + if viper.IsSet("size") { + size = new(viper.GetString("size")) + } + var partition *string + if viper.IsSet("partition") { + partition = new(viper.GetString("partition")) + } + var hostname *string + if viper.IsSet("hostname") { + hostname = new(viper.GetString("hostname")) + } + var description *string + if viper.IsSet("description") { + description = new(viper.GetString("description")) + } + + return &apiv2.MachineServiceCreateRequest{ + Description: description, + Partition: partition, + Hostname: hostname, + Image: viper.GetString("image"), + Name: viper.GetString("name"), + Project: viper.GetString("project"), + Size: size, + SshPublicKeys: keys, + Labels: labels, + Userdata: new(userDataArgument), + Networks: networks, + DnsServers: dnsServers, + NtpServers: ntpServers, + FilesystemLayout: filesystemlayout, + PlacementTags: viper.GetStringSlice("placement-tags"), + AllocationType: allocationType, + FirewallSpec: firewallSpec, + }, nil +} + +func MachineUpdateRequestFromCLI(c *config.Config, args []string) (*apiv2.MachineServiceUpdateRequest, error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return nil, err + } + + updateLabels, err := UpdateLabelsFromCLI() + if err != nil { + return nil, err + } + + sshPublicKeyArgument := viper.GetString("ssh-public-key") + + if strings.HasPrefix(sshPublicKeyArgument, "@") { + var err error + sshPublicKeyArgument, err = readFromFile(c.Fs, sshPublicKeyArgument[1:]) + if err != nil { + return nil, err + } + } + + return &apiv2.MachineServiceUpdateRequest{ + Uuid: id, + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Project: c.GetProject(), + Description: pointer.PointerOrNil(viper.GetString("description")), + Labels: updateLabels, + SshPublicKeys: []string{sshPublicKeyArgument}, + }, nil +} + +var defaultSSHKeys = [...]string{"id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"} + +func SearchSSHKey() (string, error) { + currentUser, err := osuser.Current() + if err != nil { + return "", fmt.Errorf("unable to determine current user for expanding userdata path:%w", err) + } + homeDir := currentUser.HomeDir + defaultDir := filepath.Join(homeDir, "/.ssh/") + var key string + for _, k := range defaultSSHKeys { + possibleKey := filepath.Join(defaultDir, k) + _, err := os.ReadFile(possibleKey) + if err == nil { + fmt.Printf("using SSH identity: %s. Another identity can be specified with --sshidentity/-p\n", + possibleKey) + key = possibleKey + break + } + } + + if key == "" { + return "", fmt.Errorf("failure to locate a SSH identity in default location (%s), "+ + "another identity can be specified with --sshidentity/-p", defaultDir) + } + return key, nil +} + +func readFromFile(fs *afero.Afero, filePath string) (string, error) { + filePath, err := expandFilepath(filePath) + if err != nil { + return "", err + } + + content, err := fs.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("unable to read from given file %q: %w", filePath, err) + } + + return strings.TrimSpace(string(content)), nil +} + +func expandFilepath(filePath string) (string, error) { + currentUser, err := osuser.Current() + if err != nil { + return "", fmt.Errorf("unable to determine current user for expanding userdata path:%w", err) + } + homeDir := currentUser.HomeDir + + if filePath == "~" { + filePath = homeDir + } else if strings.HasPrefix(filePath, "~/") { + filePath = filepath.Join(homeDir, filePath[2:]) + } + + return filePath, nil +} + +func parseNetworks(possibleNetworks []string) ([]*apiv2.MachineAllocationNetwork, error) { + var result []*apiv2.MachineAllocationNetwork + + for _, n := range possibleNetworks { + if n == "" { + continue + } + man := &apiv2.MachineAllocationNetwork{ + Network: n, + } + nw, ipsString, found := strings.Cut(n, ":") + if found { + man.Network = nw + for ip := range strings.SplitSeq(ipsString, ";") { + if ip == "" { + continue + } + _, err := netip.ParseAddr(ip) + if err != nil { + return nil, fmt.Errorf("malformed ip %q: %w", ip, err) + } + man.Ips = append(man.Ips, ip) + } + } + result = append(result, man) + } + + return result, nil +} + +func AddMachineCreateFlags(cmd *cobra.Command, name string, completion *completion.Completion) { + cmd.Flags().String("description", "", "Description of the "+name+" to create. [optional]") + cmd.Flags().String("partition", "", "partition/datacenter where the "+name+" is created. [required, except for reserved machines]") + cmd.Flags().String("hostname", "", "Hostname of the "+name+". [required]") + cmd.Flags().String("image", "", "OS Image to install. [required]") + cmd.Flags().String("filesystem-layout", "", "Filesystemlayout to use during machine installation. [optional]") + cmd.Flags().String("name", "", "Name of the "+name+". [optional]") + cmd.Flags().StringP("project", "p", "", "Project where the "+name+" should belong to. [required]") + cmd.Flags().String("size", "", "Size of the "+name+". [required, except for reserved machines]") + cmd.Flags().String("allocation-type", "machine", "allocation type, can be either machine|firewall") + cmd.Flags().StringP("ssh-public-key", "i", "", + `SSH public key for access via ssh and console. [optional] +Can be either the public key as string, or pointing to the public key file to use e.g.: "@~/.ssh/id_rsa.pub". +If ~/.ssh/[id_ed25519.pub | id_rsa.pub | id_dsa.pub] is present it will be picked as default, matching the first one in this order.`) + cmd.Flags().StringSlice("labels", []string{}, "labels to add to the "+name+", use it like: --labels \"a=b\" or --labels \"a=\".") + cmd.Flags().String("userdata", "", `cloud-init.io compatible userdata. [optional] +Can be either the userdata as string, or pointing to the userdata file to use e.g.: "@/tmp/userdata.cfg".`) + cmd.Flags().StringSlice("dns-servers", []string{}, "dns servers to add to the machine or firewall. [optional]") + cmd.Flags().StringSlice("ntp-servers", []string{}, "ntp servers to add to the machine or firewall. [optional]") + + cmd.Flags().StringSlice("networks", []string{}, + `Adds a network. Usage: [--networks NETWORK[:ip[;ip]][,NETWORK[:ip[;ip]]... +NETWORK specifies the name or id of an existing network. +IPs can be added per network colon separated, these ips must be already allocated upfront. If no ip(s) are specified per network, one ip per network is allocated. +`) + cmd.Flags().StringSlice("placement-tags", []string{}, "placement tags used for rack spreading") + + cmd.MarkFlagsMutuallyExclusive("file", "project") + cmd.MarkFlagsRequiredTogether("project", "networks", "hostname", "image") + cmd.MarkFlagsRequiredTogether("size", "partition") + + // Completion for arguments + genericcli.Must(cmd.RegisterFlagCompletionFunc("networks", completion.Network)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", completion.Partition)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", completion.Size)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", completion.Project)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("image", completion.Image)) + // FIXME implement + // genericcli.Must(cmd.RegisterFlagCompletionFunc("filesystem-layout", c.c.Completion.FilesystemLayoutListCompletion)) +} diff --git a/testing/e2e/test_cmd.go b/testing/e2e/test_cmd.go index ba36594..9843115 100644 --- a/testing/e2e/test_cmd.go +++ b/testing/e2e/test_cmd.go @@ -57,7 +57,9 @@ func NewRootCmd(t *testing.T, c *TestConfig) e2e_test.NewRootCmdFunc { viper.Reset() return cmd.NewRootCmd(&config.Config{ - Fs: fs, + Fs: &afero.Afero{ + Fs: fs, + }, Out: &out, In: in, PromptOut: io.Discard, diff --git a/tests/e2e/admin/machine_test.go b/tests/e2e/admin/machine_test.go new file mode 100644 index 0000000..efa33e3 --- /dev/null +++ b/tests/e2e/admin/machine_test.go @@ -0,0 +1,546 @@ +package admin_e2e + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + "testing" + + "connectrpc.com/connect" + "github.com/metal-stack/api/go/client" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + e2erootcmd "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/e2e" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func Test_MachineCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceListResponse, apiv2.Machine]{ + { + Name: "list", + CmdArgs: []string{"admin", "machine", "list", + "--name", "name", + "--hostname", "hostname", + "--image", "image", + "--partition", "partition", + "--project", "project", + "--size", "size", + "--id", "uuid", + }, + AssertExhaustiveArgs: true, + AssertExhaustiveExcludes: []string{"sort-by"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.MachineServiceListRequest{ + Query: &apiv2.MachineQuery{ + Allocation: &apiv2.MachineAllocationQuery{ + Hostname: new("hostname"), + Name: new("name"), + Image: new("image"), + Project: new("project"), + }, + Partition: new("partition"), + Size: new("size"), + Uuid: new("uuid"), + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.MachineServiceListResponse{ + Machines: []*apiv2.Machine{ + testresources.Machine2(), + testresources.Machine1(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 5fa2bbe1-407c-4142-92d5-e4419daf9646 Alive 1m v1-medium-x86 partition-1 rack-1 + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + WantWideTable: new(` + ID LAST EVENT WHEN AGE DESCRIPTION NAME HOSTNAME PROJECT IPS SIZE IMAGE PARTITION RACK STARTED TAGS STATE + 5fa2bbe1-407c-4142-92d5-e4419daf9646 Alive 1m v1-medium-x86 partition-1 rack-1 a=b available + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd Phoned Home 1m 1m machine 2 machine-2 machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c 4.5.6.7 v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 1999-12-31T23:59:00Z c=d available + 192.1.1.1 + `), + Template: new("{{ .allocation.uuid }} {{ .allocation.project }}"), + WantTemplate: new(` + +4f94e87b-b08f-4f82-b053-9b8305de60ad f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c + `), + WantMarkdown: new(` + | ID | | LAST EVENT | WHEN | AGE | HOSTNAME | PROJECT | SIZE | IMAGE | PARTITION | RACK | + |---------------------------------------|---|-------------|------|-----|-----------|--------------------------------------|---------------|--------------|-------------|--------| + | 5fa2bbe1-407c-4142-92d5-e4419daf9646 | | Alive | 1m | | | | v1-medium-x86 | | partition-1 | rack-1 | + | 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd | 🛡 | Phoned Home | 1m | 1m | machine-2 | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | v1-medium-x86 | Ubuntu 24.04 | partition-2 | rack-1 | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Describe(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceGetResponse, *apiv2.Machine]{ + { + Name: "describe", + CmdArgs: []string{"admin", "machine", "describe", testresources.Machine2().Uuid}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.MachineServiceGetRequest{ + Uuid: testresources.Machine2().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.MachineServiceGetResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantProtoObject: testresources.Machine2(), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + WantWideTable: new(` + ID LAST EVENT WHEN AGE DESCRIPTION NAME HOSTNAME PROJECT IPS SIZE IMAGE PARTITION RACK STARTED TAGS STATE + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd Phoned Home 1m 1m machine 2 machine-2 machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c 4.5.6.7 v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 1999-12-31T23:59:00Z c=d available + 192.1.1.1 + `), + Template: new("{{ .allocation.uuid }} {{ .allocation.project }}"), + WantTemplate: new(` + 4f94e87b-b08f-4f82-b053-9b8305de60ad f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c + `), + WantMarkdown: new(` + | ID | | LAST EVENT | WHEN | AGE | HOSTNAME | PROJECT | SIZE | IMAGE | PARTITION | RACK | + |---------------------------------------|---|-------------|------|-----|-----------|--------------------------------------|---------------|--------------|-------------|--------| + | 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd | 🛡 | Phoned Home | 1m | 1m | machine-2 | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | v1-medium-x86 | Ubuntu 24.04 | partition-2 | rack-1 | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Create(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceGetResponse, *apiv2.Machine]{ + { + Name: "create", + CmdArgs: []string{"admin", "machine", "create", + "--id", testresources.Machine2().Uuid, + "--project", testresources.Machine2().Allocation.Project, + "--networks", func() string { + var names []string + + for _, nw := range testresources.Machine2().Allocation.Networks { + nwWithIps := nw.Network + if len(nw.Ips) > 0 { + nwWithIps += ":" + strings.Join(nw.Ips, ";") + } + names = append(names, nwWithIps) + } + + return strings.Join(names, ",") + }(), + "--allocation-type", testresources.Machine2().Allocation.AllocationType.String(), + "--description", testresources.Machine2().Allocation.Description, + "--dns-servers", "1.1.1.1", + "--ntp-servers", "2.2.2.2,3.3.3.3", + "--filesystem-layout", "fsl1", + "--hostname", testresources.Machine2().Allocation.Hostname, + "--image", testresources.Image1().Id, + "--name", testresources.Machine2().Allocation.Name, + "--partition", testresources.Machine2().Partition.Id, + "--size", testresources.Machine2().Size.Id, + "--ssh-public-key", "@.ssh/id_rsa.pub", + "--labels", "a=b", + "--userdata", "@ignition.json", + "--placement-tags", "cluster-id=cluster-uuid", + }, + AssertExhaustiveArgs: true, + AssertExhaustiveExcludes: e2e.CommonExcludedFileArgs(), + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + genericcli.Must(fs.WriteFile(".ssh/id_rsa.pub", []byte("12345"), os.ModeAppend)) + genericcli.Must(fs.WriteFile("ignition.json", []byte("{}"), os.ModeAppend)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Uuid: &testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Partition: &testresources.Machine2().Partition.Id, + Size: &testresources.Machine2().Size.Id, + Image: testresources.Machine2().Allocation.Image.Id, + FilesystemLayout: new("fsl1"), + SshPublicKeys: []string{"12345"}, + Userdata: new(base64.StdEncoding.EncodeToString([]byte("{}"))), + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + PlacementTags: []string{"cluster-id=cluster-uuid"}, + DnsServers: []*apiv2.DNSServer{{Ip: "1.1.1.1"}}, + NtpServers: []*apiv2.NTPServer{{Address: "2.2.2.2"}, {Address: "3.3.3.3"}}, + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceCreateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantProtoObject: testresources.Machine2(), + }, + { + Name: "create from file", + CmdArgs: append([]string{"admin", "machine", "create"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Uuid: &testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Image: testresources.Machine2().Allocation.Image.Id, + Userdata: nil, + Labels: testresources.Machine2().Meta.Labels, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceCreateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Delete(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceDeleteResponse, *apiv2.Machine]{ + { + Name: "delete", + CmdArgs: []string{"admin", "machine", "delete", testresources.Machine2().Uuid}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.MachineServiceDeleteRequest{ + Uuid: testresources.Machine2().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.MachineServiceDeleteResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantProtoObject: testresources.Machine2(), + }, + { + Name: "delete from file", + CmdArgs: append([]string{"admin", "machine", "delete"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.MachineServiceDeleteRequest{ + Uuid: testresources.Machine2().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.MachineServiceDeleteResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Update(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceUpdateResponse, *apiv2.Machine]{ + { + Name: "update", + CmdArgs: []string{"admin", "machine", "update", + "--project", testresources.Project2().Uuid, + testresources.Machine2().Uuid, + "--description", "42", + "--labels", "1=2", + "--ssh-public-key", "@.ssh/id_rsa.pub", + }, + AssertExhaustiveArgs: true, + AssertExhaustiveExcludes: append(e2e.CommonExcludedFileArgs(), "add-labels", "remove-labels"), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + genericcli.Must(fs.WriteFile(".ssh/id_rsa.pub", []byte("12345"), os.ModeAppend)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceUpdateRequest{ + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Uuid: testresources.Machine2().Uuid, + Project: testresources.Project2().Uuid, + Description: new("42"), + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: &apiv2.Labels{ + Labels: map[string]string{ + "1": "2", + }, + }, + }, + }, + SshPublicKeys: []string{"12345"}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceUpdateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantProtoObject: testresources.Machine2(), + }, + { + Name: "update from file", + CmdArgs: append([]string{"admin", "machine", "update"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceUpdateRequest{ + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Uuid: testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Description: &testresources.Machine2().Allocation.Description, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: testresources.Machine2().Meta.Labels, + }, + }, + SshPublicKeys: []string{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceUpdateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Apply(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceUpdateResponse, *apiv2.Machine]{ + { + Name: "apply", + CmdArgs: append([]string{"admin", "machine", "apply"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Uuid: &testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Image: testresources.Machine2().Allocation.Image.Id, + Userdata: nil, + Labels: testresources.Machine2().Meta.Labels, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceCreateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + { + Name: "apply already exists", + CmdArgs: append([]string{"admin", "machine", "apply"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Uuid: &testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Image: testresources.Machine2().Allocation.Image.Id, + Userdata: nil, + Labels: testresources.Machine2().Meta.Labels, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), + }, + { + WantRequest: &apiv2.MachineServiceUpdateRequest{ + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Uuid: testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Description: &testresources.Machine2().Allocation.Description, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: testresources.Machine2().Meta.Labels, + }, + }, + SshPublicKeys: []string{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceUpdateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/tests/e2e/admin/switch_test.go b/tests/e2e/admin/switch_test.go index b91de20..300b8d3 100644 --- a/tests/e2e/admin/switch_test.go +++ b/tests/e2e/admin/switch_test.go @@ -40,6 +40,7 @@ meta: createdAt: "2000-01-01T00:00:00Z" description: leaf switch 2 rack: rack-1 +room: room-2 partition: fra-equ01 managementIp: 10.0.0.2 managementUser: admin @@ -289,6 +290,7 @@ switch: createdAt: "2000-01-01T00:00:00Z" description: leaf switch 2 rack: rack-1 + room: room-2 partition: fra-equ01 managementIp: 10.0.0.2 managementUser: admin diff --git a/tests/e2e/api/machine_test.go b/tests/e2e/api/machine_test.go new file mode 100644 index 0000000..079a713 --- /dev/null +++ b/tests/e2e/api/machine_test.go @@ -0,0 +1,525 @@ +package api_e2e + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + "testing" + + "connectrpc.com/connect" + "github.com/metal-stack/api/go/client" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + e2erootcmd "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/e2e" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func Test_MachineCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceListResponse, apiv2.Machine]{ + { + Name: "list", + CmdArgs: []string{"machine", "list", "--project", testresources.Project2().Uuid}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceListRequest{ + Project: testresources.Project2().Uuid, + Query: &apiv2.MachineQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceListResponse{ + Machines: []*apiv2.Machine{ + testresources.Machine2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + WantWideTable: new(` + ID LAST EVENT WHEN AGE DESCRIPTION NAME HOSTNAME PROJECT IPS SIZE IMAGE PARTITION RACK STARTED TAGS STATE + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd Phoned Home 1m 1m machine 2 machine-2 machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c 4.5.6.7 v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 1999-12-31T23:59:00Z c=d available + 192.1.1.1 + `), + Template: new("{{ .allocation.uuid }} {{ .allocation.project }}"), + WantTemplate: new(` + 4f94e87b-b08f-4f82-b053-9b8305de60ad f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c + `), + WantMarkdown: new(` + | ID | | LAST EVENT | WHEN | AGE | HOSTNAME | PROJECT | SIZE | IMAGE | PARTITION | RACK | + |---------------------------------------|---|-------------|------|-----|-----------|--------------------------------------|---------------|--------------|-------------|--------| + | 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd | 🛡 | Phoned Home | 1m | 1m | machine-2 | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | v1-medium-x86 | Ubuntu 24.04 | partition-2 | rack-1 | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Describe(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceGetResponse, *apiv2.Machine]{ + { + Name: "describe", + CmdArgs: []string{"machine", "describe", "--project", testresources.Project2().Uuid, testresources.Machine2().Uuid}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceGetRequest{ + Uuid: testresources.Machine2().Uuid, + Project: testresources.Project2().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceGetResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantProtoObject: testresources.Machine2(), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + WantWideTable: new(` + ID LAST EVENT WHEN AGE DESCRIPTION NAME HOSTNAME PROJECT IPS SIZE IMAGE PARTITION RACK STARTED TAGS STATE + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd Phoned Home 1m 1m machine 2 machine-2 machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c 4.5.6.7 v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 1999-12-31T23:59:00Z c=d available + 192.1.1.1 + `), + Template: new("{{ .allocation.uuid }} {{ .allocation.project }}"), + WantTemplate: new(` + 4f94e87b-b08f-4f82-b053-9b8305de60ad f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c + `), + WantMarkdown: new(` + | ID | | LAST EVENT | WHEN | AGE | HOSTNAME | PROJECT | SIZE | IMAGE | PARTITION | RACK | + |---------------------------------------|---|-------------|------|-----|-----------|--------------------------------------|---------------|--------------|-------------|--------| + | 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd | 🛡 | Phoned Home | 1m | 1m | machine-2 | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | v1-medium-x86 | Ubuntu 24.04 | partition-2 | rack-1 | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Create(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceGetResponse, *apiv2.Machine]{ + { + Name: "create", + CmdArgs: []string{"machine", "create", + "--project", testresources.Machine2().Allocation.Project, + "--networks", func() string { + var names []string + + for _, nw := range testresources.Machine2().Allocation.Networks { + nwWithIps := nw.Network + if len(nw.Ips) > 0 { + nwWithIps += ":" + strings.Join(nw.Ips, ";") + } + names = append(names, nwWithIps) + } + + return strings.Join(names, ",") + }(), + "--allocation-type", testresources.Machine2().Allocation.AllocationType.String(), + "--description", testresources.Machine2().Allocation.Description, + "--dns-servers", "1.1.1.1", + "--ntp-servers", "2.2.2.2,3.3.3.3", + "--filesystem-layout", "fsl1", + "--hostname", testresources.Machine2().Allocation.Hostname, + "--image", testresources.Image1().Id, + "--name", testresources.Machine2().Allocation.Name, + "--partition", testresources.Machine2().Partition.Id, + "--size", testresources.Machine2().Size.Id, + "--ssh-public-key", "@.ssh/id_rsa.pub", + "--labels", "a=b", + "--userdata", "@ignition.json", + "--placement-tags", "cluster-id=cluster-uuid", + }, + AssertExhaustiveArgs: true, + AssertExhaustiveExcludes: e2e.CommonExcludedFileArgs(), + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + genericcli.Must(fs.WriteFile(".ssh/id_rsa.pub", []byte("12345"), os.ModeAppend)) + genericcli.Must(fs.WriteFile("ignition.json", []byte("{}"), os.ModeAppend)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Partition: &testresources.Machine2().Partition.Id, + Size: &testresources.Machine2().Size.Id, + Image: testresources.Machine2().Allocation.Image.Id, + FilesystemLayout: new("fsl1"), + SshPublicKeys: []string{"12345"}, + Userdata: new(base64.StdEncoding.EncodeToString([]byte("{}"))), + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + PlacementTags: []string{"cluster-id=cluster-uuid"}, + DnsServers: []*apiv2.DNSServer{{Ip: "1.1.1.1"}}, + NtpServers: []*apiv2.NTPServer{{Address: "2.2.2.2"}, {Address: "3.3.3.3"}}, + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceCreateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantProtoObject: testresources.Machine2(), + }, + { + Name: "create from file", + CmdArgs: append([]string{"machine", "create"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Partition: &testresources.Machine2().Partition.Id, + Size: &testresources.Machine2().Size.Id, + Image: testresources.Machine2().Allocation.Image.Id, + Userdata: nil, + Labels: testresources.Machine2().Meta.Labels, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceCreateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Delete(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceDeleteResponse, *apiv2.Machine]{ + { + Name: "delete", + CmdArgs: []string{"machine", "delete", "--project", testresources.Project2().Uuid, testresources.Machine2().Uuid}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceDeleteRequest{ + Project: testresources.Project2().Uuid, + Uuid: testresources.Machine2().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceDeleteResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }), + WantProtoObject: testresources.Machine2(), + }, + { + Name: "delete from file", + CmdArgs: append([]string{"machine", "delete"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceDeleteRequest{ + Uuid: testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceDeleteResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Update(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceUpdateResponse, *apiv2.Machine]{ + { + Name: "update", + CmdArgs: []string{"machine", "update", + "--project", testresources.Project2().Uuid, + testresources.Machine2().Uuid, + "--description", "42", + "--labels", "1=2", + "--ssh-public-key", "@.ssh/id_rsa.pub", + }, + AssertExhaustiveArgs: true, + AssertExhaustiveExcludes: append(e2e.CommonExcludedFileArgs(), "add-labels", "remove-labels"), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + genericcli.Must(fs.WriteFile(".ssh/id_rsa.pub", []byte("12345"), os.ModeAppend)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceUpdateRequest{ + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Uuid: testresources.Machine2().Uuid, + Project: testresources.Project2().Uuid, + Description: new("42"), + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: &apiv2.Labels{ + Labels: map[string]string{ + "1": "2", + }, + }, + }, + }, + SshPublicKeys: []string{"12345"}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceUpdateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantProtoObject: testresources.Machine2(), + }, + { + Name: "update from file", + CmdArgs: append([]string{"machine", "update"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceUpdateRequest{ + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Uuid: testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Description: &testresources.Machine2().Allocation.Description, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: testresources.Machine2().Meta.Labels, + }, + }, + SshPublicKeys: []string{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceUpdateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_MachineCmd_Apply(t *testing.T) { + tests := []*e2e.Test[apiv2.MachineServiceUpdateResponse, *apiv2.Machine]{ + { + Name: "apply", + CmdArgs: append([]string{"machine", "apply"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Partition: &testresources.Machine2().Partition.Id, + Size: &testresources.Machine2().Size.Id, + Image: testresources.Machine2().Allocation.Image.Id, + Userdata: nil, + Labels: testresources.Machine2().Meta.Labels, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceCreateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + { + Name: "apply already exists", + CmdArgs: append([]string{"machine", "apply"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Machine2()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.MachineServiceCreateRequest{ + Project: testresources.Machine2().Allocation.Project, + Name: testresources.Machine2().Allocation.Name, + Description: &testresources.Machine2().Allocation.Description, + Hostname: &testresources.Machine2().Allocation.Hostname, + Partition: &testresources.Machine2().Partition.Id, + Size: &testresources.Machine2().Size.Id, + Image: testresources.Machine2().Allocation.Image.Id, + Userdata: nil, + Labels: testresources.Machine2().Meta.Labels, + Networks: func() []*apiv2.MachineAllocationNetwork { + var nws []*apiv2.MachineAllocationNetwork + + for _, nw := range testresources.Machine2().Allocation.Networks { + nws = append(nws, &apiv2.MachineAllocationNetwork{ + Network: nw.Network, + Ips: nw.Ips, + }) + } + + return nws + }(), + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallSpec: nil, + }, + WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), + }, + { + WantRequest: &apiv2.MachineServiceUpdateRequest{ + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_SERVER, + }, + Uuid: testresources.Machine2().Uuid, + Project: testresources.Machine2().Allocation.Project, + Description: &testresources.Machine2().Allocation.Description, + Labels: &apiv2.UpdateLabels{ + Strategy: &apiv2.UpdateLabels_Replace{ + Replace: testresources.Machine2().Meta.Labels, + }, + }, + SshPublicKeys: []string{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.MachineServiceUpdateResponse{ + Machine: testresources.Machine2(), + }) + }, + }, + }, + }, + ), + WantTable: new(` + ID LAST EVENT WHEN AGE HOSTNAME PROJECT SIZE IMAGE PARTITION RACK + 673fc473-63ca-4ea4-b9dd-b45cb2127a6fd 🛡 Phoned Home 1m 1m machine-2 f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c v1-medium-x86 Ubuntu 24.04 partition-2 rack-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/tests/e2e/testresources/machine.go b/tests/e2e/testresources/machine.go new file mode 100644 index 0000000..68089c9 --- /dev/null +++ b/tests/e2e/testresources/machine.go @@ -0,0 +1,182 @@ +package testresources + +import ( + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/genericcli/e2e" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + Machine1 = func() *apiv2.Machine { + return &apiv2.Machine{ + Uuid: "5fa2bbe1-407c-4142-92d5-e4419daf9646", + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + }, + Partition: Partition1(), + Rack: *Switch1().Rack, + Room: *Switch1().Room, + Size: Size1(), + Hardware: &apiv2.MachineHardware{ + Memory: 1024, + Disks: []*apiv2.MachineBlockDevice{ + { + Name: "/dev/sda", + Size: 1, + }, + }, + Cpus: []*apiv2.MetalCPU{ + { + Vendor: "Intel", + Model: "Xeon", + Cores: 4, + Threads: 4, + }, + }, + Gpus: []*apiv2.MetalGPU{}, + Nics: []*apiv2.MachineNic{}, + }, + Allocation: nil, + Status: &apiv2.MachineStatus{ + Condition: &apiv2.MachineCondition{ + State: apiv2.MachineState_MACHINE_STATE_AVAILABLE, + }, + LedState: &apiv2.MachineChassisIdentifyLEDState{ + Value: "", + Description: "", + }, + Liveliness: apiv2.MachineLiveliness_MACHINE_LIVELINESS_ALIVE, + MetalHammerVersion: "1", + }, + RecentProvisioningEvents: &apiv2.MachineRecentProvisioningEvents{ + Events: []*apiv2.MachineProvisioningEvent{ + { + Time: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Minute)), + Event: apiv2.MachineProvisioningEventType_MACHINE_PROVISIONING_EVENT_TYPE_ALIVE, + Message: "alive", + }, + }, + LastEventTime: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Minute)), + LastErrorEvent: &apiv2.MachineProvisioningEvent{ + Time: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Hour)), + Event: apiv2.MachineProvisioningEventType_MACHINE_PROVISIONING_EVENT_TYPE_WAITING, + Message: "waiting", + }, + State: apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_UNSPECIFIED, + }, + } + } + Machine2 = func() *apiv2.Machine { + return &apiv2.Machine{ + Uuid: "673fc473-63ca-4ea4-b9dd-b45cb2127a6fd", + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "c": "d", + }, + }, + }, + Partition: Partition2(), + Rack: *Switch2().Rack, + Room: *Switch2().Room, + Size: Size1(), + Hardware: &apiv2.MachineHardware{ + Memory: 1024, + Disks: []*apiv2.MachineBlockDevice{ + { + Name: "/dev/sda", + Size: 1, + }, + }, + Cpus: []*apiv2.MetalCPU{ + { + Vendor: "Intel", + Model: "Xeon", + Cores: 4, + Threads: 4, + }, + }, + Gpus: []*apiv2.MetalGPU{}, + Nics: []*apiv2.MachineNic{}, + }, + Allocation: &apiv2.MachineAllocation{ + Uuid: "4f94e87b-b08f-4f82-b053-9b8305de60ad", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Minute)), + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "e": "f", + }, + }, + }, + Name: "machine-2", + Description: "machine 2", + CreatedBy: "foo", + Project: Project2().Uuid, + Image: Image1(), + FilesystemLayout: &apiv2.FilesystemLayout{}, + Networks: []*apiv2.MachineNetwork{ + { + Network: Network1().Id, + Ips: []string{"4.5.6.7"}, + }, + { + Network: Network2().Id, + Ips: []string{"192.1.1.1"}, + }, + }, + Hostname: "machine-2", + SshPublicKeys: []string{}, + Userdata: "", + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + FirewallRules: nil, + DnsServers: []*apiv2.DNSServer{}, + NtpServers: []*apiv2.NTPServer{}, + Vpn: &apiv2.MachineVPN{ + ControlPlaneAddress: "1.2.3.4", + AuthKey: "abc", + Connected: true, + Ips: []string{"3.4.5.6"}, + }, + }, + Status: &apiv2.MachineStatus{ + Condition: &apiv2.MachineCondition{ + State: apiv2.MachineState_MACHINE_STATE_AVAILABLE, + }, + LedState: &apiv2.MachineChassisIdentifyLEDState{ + Value: "", + Description: "", + }, + Liveliness: apiv2.MachineLiveliness_MACHINE_LIVELINESS_ALIVE, + MetalHammerVersion: "1", + }, + RecentProvisioningEvents: &apiv2.MachineRecentProvisioningEvents{ + Events: []*apiv2.MachineProvisioningEvent{ + { + Time: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Minute)), + Event: apiv2.MachineProvisioningEventType_MACHINE_PROVISIONING_EVENT_TYPE_PHONED_HOME, + Message: "phoned home", + }, + { + Time: timestamppb.New(e2e.TimeBubbleStartTime().Add(-2 * time.Minute)), + Event: apiv2.MachineProvisioningEventType_MACHINE_PROVISIONING_EVENT_TYPE_ALIVE, + Message: "alive", + }, + }, + LastEventTime: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Minute)), + LastErrorEvent: &apiv2.MachineProvisioningEvent{ + Time: timestamppb.New(e2e.TimeBubbleStartTime().Add(-1 * time.Hour)), + Event: apiv2.MachineProvisioningEventType_MACHINE_PROVISIONING_EVENT_TYPE_WAITING, + Message: "waiting", + }, + State: apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_UNSPECIFIED, + }, + } + } +) diff --git a/tests/e2e/testresources/switch.go b/tests/e2e/testresources/switch.go index 7564b02..09ad012 100644 --- a/tests/e2e/testresources/switch.go +++ b/tests/e2e/testresources/switch.go @@ -15,11 +15,12 @@ var ( Id: "leaf01", Partition: "fra-equ01", Rack: new("rack-1"), + Room: new("room-1"), Description: "leaf switch 1", ManagementIp: "10.0.0.1", ManagementUser: new("admin"), MachineConnections: []*apiv2.MachineConnection{ - &apiv2.MachineConnection{ + { MachineId: "id1", Nic: Nic1(), }, @@ -44,6 +45,7 @@ var ( Id: "leaf02", Partition: "fra-equ01", Rack: new("rack-1"), + Room: new("room-2"), Description: "leaf switch 2", ManagementIp: "10.0.0.2", ManagementUser: new("admin"),