diff --git a/cmd/admin/v2/commands.go b/cmd/admin/v2/commands.go index 70667cb..2e487e2 100644 --- a/cmd/admin/v2/commands.go +++ b/cmd/admin/v2/commands.go @@ -17,11 +17,16 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { adminCmd.AddCommand(newAuditCmd(c)) adminCmd.AddCommand(newComponentCmd(c)) adminCmd.AddCommand(newImageCmd(c)) + adminCmd.AddCommand(newIPCmd(c)) + adminCmd.AddCommand(newMachineCmd(c)) + adminCmd.AddCommand(newNetworkCmd(c)) adminCmd.AddCommand(newProjectCmd(c)) + adminCmd.AddCommand(newSizeCmd(c)) adminCmd.AddCommand(newSwitchCmd(c)) adminCmd.AddCommand(newTaskCmd(c)) adminCmd.AddCommand(newTenantCmd(c)) adminCmd.AddCommand(newTokenCmd(c)) + adminCmd.AddCommand(newVPNCmd(c)) cmd.AddCommand(adminCmd) } diff --git a/cmd/admin/v2/ip.go b/cmd/admin/v2/ip.go new file mode 100644 index 0000000..c512eae --- /dev/null +++ b/cmd/admin/v2/ip.go @@ -0,0 +1,132 @@ +package v2 + +import ( + "strings" + + "github.com/metal-stack/api/go/enum" + 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/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 ip struct { + c *config.Config +} + +func newIPCmd(c *config.Config) *cobra.Command { + w := &ip{ + c: c, + } + + cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.IP]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "ip", + Plural: "ips", + Description: "manage ip addresses", + Sorter: sorters.IPSorter(), + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + OnlyCmds: genericcli.OnlyCmds(genericcli.ListCmd), + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("ip", "", "ipaddress to filter [optional]") + cmd.Flags().String("name", "", "name to filter [optional]") + cmd.Flags().String("network", "", "network to filter [optional]") + cmd.Flags().String("project", "", "project to filter [optional]") + cmd.Flags().String("uuid", "", "allocation uuid to filter [optional]") + cmd.Flags().String("machine", "", "machine to filter [optional]") + cmd.Flags().String("namespace", "", "namespace to filter [optional]") + cmd.Flags().String("parent-prefix", "", "parent-prefix to filter [optional]") + cmd.Flags().String("type", "", "type to filter [optional] can be either ephemeral|static") + cmd.Flags().String("addressfamily", "", "addressfamily to filter [optional] can be either ipv6|ipv6") + cmd.Flags().StringSlice("label", nil, "label to filter, must be in the form of key=value, can be either specified multiple times, or comma seperated [optional]") + genericcli.Must(cmd.RegisterFlagCompletionFunc("network", c.Completion.NetworkListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("type", c.Completion.IpTypeCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("addressfamily", c.Completion.IpAddressFamilyCompletion)) + }, + ValidArgsFn: c.Completion.IpListCompletion, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *ip) List() ([]*apiv2.IP, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + var ( + labels *apiv2.Labels + ipType *apiv2.IPType + af *apiv2.IPAddressFamily + ) + + if len(viper.GetStringSlice("label")) > 0 { + labels = &apiv2.Labels{ + Labels: map[string]string{}, + } + for _, label := range viper.GetStringSlice("label") { + key, value, _ := strings.Cut(label, "=") + labels.Labels[key] = value + } + } + + if viper.IsSet("type") { + ipt, err := enum.GetEnum[apiv2.IPType](viper.GetString("type")) + if err != nil { + return nil, err + } + ipType = &ipt + } + + if viper.IsSet("addressfamily") { + ipaf, err := enum.GetEnum[apiv2.IPAddressFamily](viper.GetString("addressfamily")) + if err != nil { + return nil, err + } + af = &ipaf + } + + resp, err := c.c.Client.Adminv2().IP().List(ctx, &adminv2.IPServiceListRequest{ + Query: &apiv2.IPQuery{ + Ip: pointer.PointerOrNil(viper.GetString("ip")), + Network: pointer.PointerOrNil(viper.GetString("network")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Project: pointer.PointerOrNil(viper.GetString("project")), + Uuid: pointer.PointerOrNil(viper.GetString("uuid")), + Machine: pointer.PointerOrNil(viper.GetString("machine")), + ParentPrefixCidr: pointer.PointerOrNil(viper.GetString("parent-prefix")), + Namespace: pointer.PointerOrNil(viper.GetString("namespace")), + Labels: labels, + Type: ipType, + AddressFamily: af, + }, + }) + if err != nil { + return nil, err + } + + return resp.Ips, nil +} + +func (t *ip) Get(id string) (*apiv2.IP, error) { + panic("unimplemented") +} +func (c *ip) Delete(id string) (*apiv2.IP, error) { + panic("unimplemented") +} +func (t *ip) Create(rq any) (*apiv2.IP, error) { + panic("unimplemented") +} +func (t *ip) Convert(r *apiv2.IP) (string, any, any, error) { + panic("unimplemented") +} + +func (t *ip) Update(rq any) (*apiv2.IP, error) { + panic("unimplemented") +} diff --git a/cmd/admin/v2/machine.go b/cmd/admin/v2/machine.go new file mode 100644 index 0000000..ee66554 --- /dev/null +++ b/cmd/admin/v2/machine.go @@ -0,0 +1,189 @@ +package v2 + +import ( + "fmt" + + 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/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/tag" + "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[any, any, *apiv2.Machine]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "machine", + 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().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.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.PartitionListCompletion)) + }, + DescribeCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project of the machine") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + }, + ValidArgsFn: c.Completion.MachineListCompletion, + } + + 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() + }, + } + bmcCommandCmd.Flags().String("id", "", "id of the machine where the command should be sent to") + bmcCommandCmd.Flags().String("command", "", "the actual command to send to the machine") + genericcli.Must(bmcCommandCmd.RegisterFlagCompletionFunc("id", c.Completion.MachineListCompletion)) + genericcli.Must(bmcCommandCmd.RegisterFlagCompletionFunc("command", c.Completion.BMCCommandListCompletion)) + genericcli.Must(bmcCommandCmd.MarkFlagRequired("id")) + genericcli.Must(bmcCommandCmd.MarkFlagRequired("command")) + + return genericcli.NewCmds(cmdsConfig, bmcCommandCmd) +} + +func (c *machine) bmcCommand() error { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + commandString := viper.GetString("command") + + cmd, ok := apiv2.MachineBMCCommand_value[commandString] + if !ok { + return fmt.Errorf("unknown command: %s", commandString) + } + _, err := c.c.Client.Adminv2().Machine().BMCCommand(ctx, &adminv2.MachineServiceBMCCommandRequest{ + Uuid: viper.GetString("id"), + Command: apiv2.MachineBMCCommand(cmd), + }) + if err != nil { + return err + } + return err +} + +func (c *machine) Create(rq any) (*apiv2.Machine, error) { + panic("unimplemented") +} + +func (c *machine) Delete(id string) (*apiv2.Machine, error) { + panic("unimplemented") +} + +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() + + resp, err := c.c.Client.Adminv2().Machine().List(ctx, &adminv2.MachineServiceListRequest{ + 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")), + Rack: pointer.PointerOrNil(viper.GetString("rack")), + Labels: &apiv2.Labels{ + Labels: tag.NewTagMap(viper.GetStringSlice("labels")), + }, + Allocation: &apiv2.MachineAllocationQuery{ + Uuid: pointer.PointerOrNil(viper.GetString("allocation-uuid")), + Name: pointer.PointerOrNil(viper.GetString("allocation-name")), + Project: pointer.PointerOrNil(viper.GetString("project")), + Image: pointer.PointerOrNil(viper.GetString("image")), + FilesystemLayout: pointer.PointerOrNil(viper.GetString("file-system-layout-id")), + Hostname: pointer.PointerOrNil(viper.GetString("hostname")), + // AllocationType: &0, + Labels: &apiv2.Labels{ + Labels: tag.NewTagMap(viper.GetStringSlice("allocation-labels")), + }, + // Vpn: &apiv2.MachineVPN{}, these query fields are no pointers and somehow seem wrong? how to search for vpn key? + }, + Network: &apiv2.MachineNetworkQuery{}, + Nic: &apiv2.MachineNicQuery{}, + Disk: &apiv2.MachineDiskQuery{ + Names: viper.GetStringSlice("disk-names"), + // Sizes: + }, + 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, + }, + Partition: nil, // again partition? + }) + if err != nil { + return nil, err + } + + return resp.Machines, nil +} + +func (c *machine) Update(rq any) (*apiv2.Machine, error) { + panic("unimplemented") +} + +func (c *machine) Convert(r *apiv2.Machine) (string, any, any, error) { + panic("unimplemented") + +} + +func (c *machine) MachineResponseToCreate(r *apiv2.Machine) any { + panic("unimplemented") +} + +func (c *machine) MachineResponseToUpdate(desired *apiv2.Machine) (any, error) { + panic("unimplemented") +} diff --git a/cmd/admin/v2/network.go b/cmd/admin/v2/network.go new file mode 100644 index 0000000..6fa4f90 --- /dev/null +++ b/cmd/admin/v2/network.go @@ -0,0 +1,397 @@ +package v2 + +import ( + "github.com/metal-stack/api/go/enum" + 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/common" + "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/metal-stack/metal-lib/pkg/tag" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type networkCmd struct { + c *config.Config +} + +func newNetworkCmd(c *config.Config) *cobra.Command { + w := &networkCmd{ + c: c, + } + + // TODO: move to common? + listFlags := func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "ID to filter [optional]") + cmd.Flags().String("name", "", "name to filter [optional]") + cmd.Flags().String("description", "", "description to filter [optional]") + cmd.Flags().String("partition", "", "partition to filter [optional]") + cmd.Flags().String("project", "", "project to filter [optional]") + cmd.Flags().StringSlice("prefixes", []string{}, "prefixes to filter") + cmd.Flags().StringSlice("destination-prefixes", []string{}, "destination prefixes to filter") + cmd.Flags().String("addressfamily", "", "addressfamily to filter, either ipv4 or ipv6 [optional]") + cmd.Flags().Uint32("vrf", 0, "vrf to filter [optional]") + cmd.Flags().StringSlice("labels", nil, "labels to filter [optional]") + cmd.Flags().StringP("type", "t", "", "type of the network. [optional]") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.PartitionListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("addressfamily", c.Completion.NetworkAddressFamilyCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("type", c.Completion.NetworkTypeCompletion)) + } + + cmdsConfig := &genericcli.CmdsConfig[*adminv2.NetworkServiceCreateRequest, *adminv2.NetworkServiceUpdateRequest, *apiv2.Network]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "network", + Plural: "networks", + Description: "networks can be attached to a machine or firewall such that they can communicate with each other.", + CreateRequestFromCLI: w.createRequestFromCLI, + UpdateRequestFromCLI: w.updateRequestFromCLI, + Sorter: sorters.NetworkSorter(), + ValidArgsFn: c.Completion.NetworkAdminListCompletion, + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "id of the network to create, defaults to a random uuid if not provided. [optional]") + cmd.Flags().String("name", "", "name of the network to create. [required]") + cmd.Flags().StringP("type", "t", "", "type of the network. [required]") + cmd.Flags().String("nat-type", "", "nat-type of the network. [required]") + cmd.Flags().String("partition", "", "partition where this network should exist. [required]") + cmd.Flags().String("project", "", "partition where this network should exist (alternative to parent-network-id). [optional]") + cmd.Flags().String("parent-network-id", "", "the parent of the network (alternative to partition). [optional]") + cmd.Flags().String("description", "", "description of the network to create. [optional]") + cmd.Flags().StringSlice("labels", nil, "labels for this network. [optional]") + cmd.Flags().String("addressfamily", "", "addressfamily of the network to acquire, if not specified the network inherits the address families from the parent [optional]") + cmd.Flags().Uint32("ipv4-prefix-length", 0, "ipv4 prefix bit length of the network to create, defaults to default child prefix length of the parent network. [optional]") + cmd.Flags().Uint32("ipv6-prefix-length", 0, "ipv6 prefix bit length of the network to create, defaults to default child prefix length of the parent network. [optional]") + cmd.Flags().Uint32("default-ipv4-prefix-length", 0, "default ipv4 prefix bit length of the network to create. [optional]") + cmd.Flags().Uint32("default-ipv6-prefix-length", 0, "default ipv6 prefix bit length of the network to create. [optional]") + cmd.Flags().Uint32("min-ipv4-prefix-length", 0, "min ipv4 prefix bit length of the network to create. [optional]") + cmd.Flags().Uint32("min-ipv6-prefix-length", 0, "min ipv6 prefix bit length of the network to create. [optional]") + cmd.Flags().StringSlice("prefixes", nil, "prefixes for this network. [optional]") + cmd.Flags().StringSlice("destination-prefixes", nil, "destination-prefixes for this network. [optional]") + cmd.Flags().StringSlice("additional-announcable-cidrs", nil, "additional-announcable-cidrs for this network. [optional]") + cmd.Flags().Uint32("vrf", 0, "the vrf of the network to create. [optional]") + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.PartitionListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("addressfamily", c.Completion.NetworkAddressFamilyCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("type", c.Completion.NetworkTypeCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("nat-type", c.Completion.NetworkNatTypeCompletion)) + }, + ListCmdMutateFn: func(cmd *cobra.Command) { + listFlags(cmd) + cmd.Flags().String("parent-network-id", "", "parent network to filter [optional]") + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("name", "", "the name of the network [optional]") + cmd.Flags().String("description", "", "the description of the network [optional]") + cmd.Flags().StringSlice("labels", nil, "the labels of the network, must be in the form of key=value, use it like: --labels \"key1=value1,key2=value2\". [optional]") + cmd.Flags().String("project", "", "project to filter [optional]") + }, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *networkCmd) Get(id string) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Network().Get(ctx, &adminv2.NetworkServiceGetRequest{ + Id: id, + }) + + if err != nil { + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) List() ([]*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + var nwType *apiv2.NetworkType + if viper.IsSet("type") { + nt, err := enum.GetEnum[apiv2.NetworkType](viper.GetString("type")) + if err != nil { + return nil, err + } + nwType = &nt + } + + resp, err := c.c.Client.Adminv2().Network().List(ctx, &adminv2.NetworkServiceListRequest{ + Query: &apiv2.NetworkQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Project: pointer.PointerOrNil(viper.GetString("project")), + Prefixes: viper.GetStringSlice("prefixes"), + DestinationPrefixes: viper.GetStringSlice("destination-prefixes"), + Vrf: pointer.PointerOrNil(viper.GetUint32("vrf")), + ParentNetwork: pointer.PointerOrNil(viper.GetString("parent-network-id")), + AddressFamily: common.NetworkAddressFamilyToType(viper.GetString("addressfamily")), + Labels: &apiv2.Labels{ + Labels: tag.NewTagMap(viper.GetStringSlice("labels")), + }, + Type: nwType, + // NatType: (*apiv2.NATType)(nwType), + }, + }) + + if err != nil { + return nil, err + } + + return resp.Networks, nil +} + +func (c *networkCmd) Delete(id string) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Network().Delete(ctx, &adminv2.NetworkServiceDeleteRequest{ + Id: id, + }) + if err != nil { + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) Create(rq *adminv2.NetworkServiceCreateRequest) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Network().Create(ctx, rq) + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.AlreadyExists { + return nil, genericcli.AlreadyExistsError() + } + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) Update(rq *adminv2.NetworkServiceUpdateRequest) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Network().Update(ctx, rq) + if err != nil { + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) Convert(r *apiv2.Network) (string, *adminv2.NetworkServiceCreateRequest, *adminv2.NetworkServiceUpdateRequest, error) { + return r.Id, networkResponseToCreate(r), networkResponseToUpdate(r), nil +} + +func networkResponseToCreate(r *apiv2.Network) *adminv2.NetworkServiceCreateRequest { + meta := pointer.SafeDeref(r.Meta) + + return &adminv2.NetworkServiceCreateRequest{ + Project: r.Project, + Name: r.Name, + Description: r.Description, + Partition: r.Partition, + Labels: &apiv2.Labels{ + Labels: pointer.SafeDeref(meta.Labels).Labels, + }, + ParentNetwork: r.ParentNetwork, + // TODO: allow defining length and addressfamilies somehow? + } +} + +func networkResponseToUpdate(r *apiv2.Network) *adminv2.NetworkServiceUpdateRequest { + meta := pointer.SafeDeref(r.Meta) + + return &adminv2.NetworkServiceUpdateRequest{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Labels: &apiv2.UpdateLabels{ + Update: meta.Labels, // TODO: this only ensures that the labels are present but it does not cleanup old one's, which would require fetching the current state and calculating the diff + }, + Prefixes: r.Prefixes, + DestinationPrefixes: r.DestinationPrefixes, + DefaultChildPrefixLength: r.DefaultChildPrefixLength, + MinChildPrefixLength: r.MinChildPrefixLength, + // NatType: &0, + AdditionalAnnouncableCidrs: r.AdditionalAnnouncableCidrs, + Force: false, + } +} + +func (c *networkCmd) createRequestFromCLI() (*adminv2.NetworkServiceCreateRequest, error) { + labels, err := genericcli.LabelsToMap(viper.GetStringSlice("labels")) + if err != nil { + return nil, err + } + + var ( + natType = apiv2.NATType_NAT_TYPE_NONE + defaultCPL *apiv2.ChildPrefixLength + minCPL *apiv2.ChildPrefixLength + length *apiv2.ChildPrefixLength + ) + if viper.IsSet("default-ipv4-prefix-length") { + defaultCPL = &apiv2.ChildPrefixLength{ + Ipv4: new(viper.GetUint32("default-ipv4-prefix-length")), + } + } + if viper.IsSet("default-ipv6-prefix-length") { + if defaultCPL == nil { + defaultCPL = &apiv2.ChildPrefixLength{} + } + defaultCPL.Ipv6 = new(viper.GetUint32("default-ipv6-prefix-length")) + } + if viper.IsSet("min-ipv4-prefix-length") { + minCPL = &apiv2.ChildPrefixLength{ + Ipv4: new(viper.GetUint32("min-ipv4-prefix-length")), + } + } + if viper.IsSet("min-ipv6-prefix-length") { + if minCPL == nil { + minCPL = &apiv2.ChildPrefixLength{} + } + minCPL.Ipv6 = new(viper.GetUint32("min-ipv6-prefix-length")) + } + if viper.IsSet("ipv4-prefix-length") { + length = &apiv2.ChildPrefixLength{ + Ipv4: new(viper.GetUint32("ipv4-prefix-length")), + } + } + if viper.IsSet("ipv6-prefix-length") { + if length == nil { + length = &apiv2.ChildPrefixLength{} + } + length.Ipv6 = new(viper.GetUint32("ipv6-prefix-length")) + } + + nwType, err := enum.GetEnum[apiv2.NetworkType](viper.GetString("type")) + if err != nil { + return nil, err + } + + if viper.IsSet("nat-type") { + natType, err = enum.GetEnum[apiv2.NATType](viper.GetString("nat-type")) + if err != nil { + return nil, err + } + } + + var vrf *uint32 + if viper.IsSet("vrf") { + vrf = new(viper.GetUint32("vrf")) + } + + return &adminv2.NetworkServiceCreateRequest{ + Description: pointer.PointerOrNil(viper.GetString("description")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Project: pointer.PointerOrNil(viper.GetString("project")), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Labels: &apiv2.Labels{ + Labels: labels, + }, + ParentNetwork: pointer.PointerOrNil(viper.GetString("parent-network-id")), + AddressFamily: common.NetworkAddressFamilyToType(viper.GetString("addressfamily")), + Id: pointer.PointerOrNil(viper.GetString("id")), + Type: nwType, + Prefixes: viper.GetStringSlice("prefixes"), + DestinationPrefixes: viper.GetStringSlice("destination-prefixes"), + DefaultChildPrefixLength: defaultCPL, + MinChildPrefixLength: minCPL, + NatType: &natType, + Vrf: vrf, + AdditionalAnnouncableCidrs: viper.GetStringSlice("additional-announcable-cidrs"), + Length: length, + }, nil +} + +func (c *networkCmd) updateRequestFromCLI(args []string) (*adminv2.NetworkServiceUpdateRequest, error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return nil, err + } + + var labels *apiv2.UpdateLabels + if viper.IsSet("labels") { + lbls, err := genericcli.LabelsToMap(viper.GetStringSlice("labels")) + if err != nil { + return nil, err + } + + labels = &apiv2.UpdateLabels{ + Update: &apiv2.Labels{ + Labels: lbls, + }, + } + } + + var ( + natType = apiv2.NATType_NAT_TYPE_NONE + defaultCPL *apiv2.ChildPrefixLength + minCPL *apiv2.ChildPrefixLength + ) + if viper.IsSet("default-ipv4-prefix-length") { + defaultCPL = &apiv2.ChildPrefixLength{ + Ipv4: new(viper.GetUint32("default-ipv4-prefix-length")), + } + } + if viper.IsSet("default-ipv6-prefix-length") { + if defaultCPL == nil { + defaultCPL = &apiv2.ChildPrefixLength{} + } + defaultCPL.Ipv6 = new(viper.GetUint32("default-ipv6-prefix-length")) + } + if viper.IsSet("min-ipv4-prefix-length") { + minCPL = &apiv2.ChildPrefixLength{ + Ipv4: new(viper.GetUint32("min-ipv4-prefix-length")), + } + } + if viper.IsSet("min-ipv6-prefix-length") { + if minCPL == nil { + minCPL = &apiv2.ChildPrefixLength{} + } + minCPL.Ipv6 = new(viper.GetUint32("min-ipv6-prefix-length")) + } + + if viper.IsSet("nat-type") { + natType, err = enum.GetEnum[apiv2.NATType](viper.GetString("nat-type")) + if err != nil { + return nil, err + } + } + + var ( + ur = &adminv2.NetworkServiceUpdateRequest{ + Id: id, + Description: pointer.PointerOrNil(viper.GetString("description")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Labels: labels, + Prefixes: viper.GetStringSlice("prefixes"), + DestinationPrefixes: viper.GetStringSlice("destination-prefixes"), + DefaultChildPrefixLength: defaultCPL, + MinChildPrefixLength: minCPL, + NatType: &natType, + AdditionalAnnouncableCidrs: viper.GetStringSlice("additional-announcable-cidrs"), + Force: viper.GetBool("force"), + } + ) + + return ur, nil +} diff --git a/cmd/admin/v2/size.go b/cmd/admin/v2/size.go new file mode 100644 index 0000000..962ad71 --- /dev/null +++ b/cmd/admin/v2/size.go @@ -0,0 +1,123 @@ +package v2 + +import ( + "fmt" + + 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/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 size struct { + c *config.Config +} + +func newSizeCmd(c *config.Config) *cobra.Command { + w := &size{ + c: c, + } + gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) + + cmdsConfig := &genericcli.CmdsConfig[*adminv2.SizeServiceCreateRequest, *adminv2.SizeServiceUpdateRequest, *apiv2.Size]{ + BinaryName: config.BinaryName, + GenericCLI: gcli, + Singular: "size", + Plural: "sizes", + Description: "manage sizes which defines the cpu, gpu, memory and storage properties of machines", + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ValidArgsFn: c.Completion.SizeListCompletion, + OnlyCmds: genericcli.OnlyCmds(genericcli.CreateCmd, genericcli.UpdateCmd, genericcli.DeleteCmd, genericcli.EditCmd), + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *size) Get(id string) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceGetRequest{Id: id} + + resp, err := c.c.Client.Apiv2().Size().Get(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) Create(rq *adminv2.SizeServiceCreateRequest) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Size().Create(ctx, rq) + if err != nil { + return nil, fmt.Errorf("failed to get size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) Delete(id string) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &adminv2.SizeServiceDeleteRequest{Id: id} + + resp, err := c.c.Client.Adminv2().Size().Delete(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to delete size: %w", err) + } + + return resp.Size, nil +} +func (c *size) List() ([]*apiv2.Size, error) { + panic("unimplemented") + +} +func (c *size) Convert(r *apiv2.Size) (string, *adminv2.SizeServiceCreateRequest, *adminv2.SizeServiceUpdateRequest, error) { + + return r.Id, &adminv2.SizeServiceCreateRequest{ + Size: &apiv2.Size{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Meta: r.Meta, + Constraints: r.Constraints, + }, + }, &adminv2.SizeServiceUpdateRequest{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Constraints: r.Constraints, + // FIXME + Labels: &apiv2.UpdateLabels{}, + }, nil + +} + +func (c *size) Update(rq *adminv2.SizeServiceUpdateRequest) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &adminv2.SizeServiceUpdateRequest{ + Id: viper.GetString("id"), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Constraints: rq.Constraints, + Labels: &apiv2.UpdateLabels{}, // FIXME + } + + resp, err := c.c.Client.Adminv2().Size().Update(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get size: %w", err) + } + + return resp.Size, nil +} diff --git a/cmd/admin/v2/tenant.go b/cmd/admin/v2/tenant.go index f385a81..bfe8d8a 100644 --- a/cmd/admin/v2/tenant.go +++ b/cmd/admin/v2/tenant.go @@ -47,8 +47,8 @@ func newTenantCmd(c *config.Config) *cobra.Command { return &adminv2.TenantServiceCreateRequest{ Name: viper.GetString("name"), Description: pointer.PointerOrNil(viper.GetString("description")), - Email: pointer.PointerOrNil(viper.GetString("email")), AvatarUrl: pointer.PointerOrNil(viper.GetString("avatar-url")), + Email: pointer.PointerOrNil(viper.GetString("email")), }, nil }, OnlyCmds: genericcli.OnlyCmds(genericcli.ListCmd, genericcli.CreateCmd), diff --git a/cmd/admin/v2/token.go b/cmd/admin/v2/token.go index 51fc70a..255cf78 100644 --- a/cmd/admin/v2/token.go +++ b/cmd/admin/v2/token.go @@ -27,7 +27,7 @@ func newTokenCmd(c *config.Config) *cobra.Command { GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "token", Plural: "tokens", - Description: "manage api tokens for accessing the metal-stack.io api", + Description: "manage api tokens", Sorter: sorters.TokenSorter(), DescribePrinter: func() printers.Printer { return c.DescribePrinter }, ListPrinter: func() printers.Printer { return c.ListPrinter }, diff --git a/cmd/admin/v2/vpn.go b/cmd/admin/v2/vpn.go new file mode 100644 index 0000000..65bcd13 --- /dev/null +++ b/cmd/admin/v2/vpn.go @@ -0,0 +1,114 @@ +package v2 + +import ( + "fmt" + "time" + + 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/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "google.golang.org/protobuf/types/known/durationpb" +) + +type vpn struct { + c *config.Config +} + +func newVPNCmd(c *config.Config) *cobra.Command { + w := &vpn{ + c: c, + } + + cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.VPNNode]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "vpn", + Plural: "vpn", + Description: "manage vpn keys and list nodes connected", + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("project", "", "the project for which vpn nodes should be listed") + }, + OnlyCmds: genericcli.OnlyCmds(genericcli.ListCmd), + ValidArgsFn: w.c.Completion.ProjectListCompletion, + } + + authKeyCmd := &cobra.Command{ + Use: "authkey", + Short: "generate a authkey to connect to the vpn", + RunE: func(cmd *cobra.Command, args []string) error { + return w.authKey() + }, + ValidArgsFunction: c.Completion.ProjectListCompletion, + } + authKeyCmd.Flags().String("project", "", "the project for which the authkey should be generated") + authKeyCmd.Flags().Bool("ephemeral", true, "ephemeral defines if the key can only be used once") + authKeyCmd.Flags().Duration("expires", 1*time.Hour, "the duration after the generated key is not valid anymore") + genericcli.Must(authKeyCmd.MarkFlagRequired("project")) + + return genericcli.NewCmds(cmdsConfig, authKeyCmd) +} + +func (v *vpn) authKey() error { + ctx, cancel := v.c.NewRequestContext() + defer cancel() + + req := &adminv2.VPNServiceAuthKeyRequest{ + Project: viper.GetString("project"), + Ephemeral: viper.GetBool("ephemeral"), + Expires: durationpb.New(viper.GetDuration("expires")), + } + + resp, err := v.c.Client.Adminv2().VPN().AuthKey(ctx, req) + if err != nil { + return err + } + + _, _ = fmt.Fprintf(v.c.Out, "authkey: %s ephemeral:%t created at:%s expires at:%s\n", resp.AuthKey, resp.Ephemeral, resp.CreatedAt, resp.ExpiresAt) + _, _ = fmt.Fprintf(v.c.Out, "vpn endpoint: %s\n", resp.Address) + + return nil +} + +func (v *vpn) Get(id string) (*apiv2.VPNNode, error) { + panic("unimplemented") +} + +func (v *vpn) List() ([]*apiv2.VPNNode, error) { + ctx, cancel := v.c.NewRequestContext() + defer cancel() + + req := &adminv2.VPNServiceListNodesRequest{} + + if viper.IsSet("project") { + req.Project = new(viper.GetString("project")) + } + + resp, err := v.c.Client.Adminv2().VPN().ListNodes(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to list vpn nodes: %w", err) + } + + return resp.Nodes, nil +} + +func (v *vpn) Create(rq any) (*apiv2.VPNNode, error) { + panic("unimplemented") +} + +func (v *vpn) Delete(id string) (*apiv2.VPNNode, error) { + panic("unimplemented") +} + +func (v *vpn) Convert(r *apiv2.VPNNode) (string, any, any, error) { + panic("unimplemented") +} + +func (v *vpn) Update(rq any) (*apiv2.VPNNode, error) { + panic("unimplemented") +} diff --git a/cmd/api/v2/commands.go b/cmd/api/v2/commands.go index d79e964..11e3c21 100644 --- a/cmd/api/v2/commands.go +++ b/cmd/api/v2/commands.go @@ -10,8 +10,11 @@ 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(newProjectCmd(c)) + cmd.AddCommand(newSizeCmd(c)) cmd.AddCommand(newTenantCmd(c)) cmd.AddCommand(newTokenCmd(c)) cmd.AddCommand(newUserCmd(c)) diff --git a/cmd/api/v2/ip.go b/cmd/api/v2/ip.go index bcc8e40..5b0c79e 100644 --- a/cmd/api/v2/ip.go +++ b/cmd/api/v2/ip.go @@ -4,6 +4,7 @@ import ( 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/common" "github.com/metal-stack/cli/pkg/helpers" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" @@ -26,7 +27,7 @@ func newIPCmd(c *config.Config) *cobra.Command { GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "ip", Plural: "ips", - Description: "an ip address of metal-stack.io", + Description: "manage ip addresses", Sorter: sorters.IPSorter(), DescribePrinter: func() printers.Printer { return c.DescribePrinter }, ListPrinter: func() printers.Printer { return c.ListPrinter }, @@ -44,7 +45,10 @@ func newIPCmd(c *config.Config) *cobra.Command { cmd.Flags().BoolP("static", "", false, "make this ip static") cmd.Flags().StringP("addressfamily", "", "", "addressfamily, can be either IPv4|IPv6, defaults to IPv4 (optional)") + genericcli.Must(cmd.RegisterFlagCompletionFunc("network", c.Completion.NetworkListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("addressfamily", c.Completion.IpAddressFamilyCompletion)) + }, UpdateCmdMutateFn: func(cmd *cobra.Command) { cmd.Flags().StringP("project", "p", "", "project of the ip") @@ -58,6 +62,7 @@ func newIPCmd(c *config.Config) *cobra.Command { }, DescribeCmdMutateFn: func(cmd *cobra.Command) { cmd.Flags().StringP("project", "p", "", "project of the ip") + cmd.Flags().StringP("namespace", "n", "", "namespace of the ip") genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) }, @@ -85,14 +90,15 @@ func (c *ip) createFromCLI() (*apiv2.IPServiceCreateRequest, error) { } labels = &apiv2.Labels{Labels: labelsMap} } + return &apiv2.IPServiceCreateRequest{ Project: c.c.GetProject(), Network: viper.GetString("network"), Name: pointer.PointerOrNil(viper.GetString("name")), Description: pointer.PointerOrNil(viper.GetString("description")), Labels: labels, - Type: new(ipStaticToType(viper.GetBool("static"))), - AddressFamily: addressFamilyToType(viper.GetString("addressfamily")), + Type: new(common.IpStaticToType(viper.GetBool("static"))), + AddressFamily: common.IPAddressFamilyToType(viper.GetString("addressfamily")), }, nil } @@ -117,7 +123,7 @@ func (c *ip) updateFromCLI(args []string) (*apiv2.IPServiceUpdateRequest, error) req.Description = pointer.PointerOrNil(viper.GetString("description")) } if viper.IsSet("static") { - req.Type = pointer.PointerOrNil(ipStaticToType(viper.GetBool("static"))) + req.Type = pointer.PointerOrNil(common.IpStaticToType(viper.GetBool("static"))) } if viper.IsSet("remove-labels") || viper.IsSet("labels") { labelsUpdate := &apiv2.UpdateLabels{} @@ -184,9 +190,17 @@ func (c *ip) Get(id string) (*apiv2.IP, error) { ctx, cancel := c.c.NewRequestContext() defer cancel() + var ( + namespace *string + ) + if viper.IsSet("namespace") { + namespace = new(viper.GetString("namespace")) + } + resp, err := c.c.Client.Apiv2().IP().Get(ctx, &apiv2.IPServiceGetRequest{ - Project: c.c.GetProject(), - Ip: id, + Project: c.c.GetProject(), + Ip: id, + Namespace: namespace, }) if err != nil { return nil, err @@ -284,23 +298,3 @@ func (c *ip) IpResponseToUpdate(r *apiv2.IP) (*apiv2.IPServiceUpdateRequest, err }, }, nil } - -func ipStaticToType(b bool) apiv2.IPType { - if b { - return apiv2.IPType_IP_TYPE_STATIC - } - return apiv2.IPType_IP_TYPE_EPHEMERAL -} - -func addressFamilyToType(af string) *apiv2.IPAddressFamily { - switch af { - case "": - return nil - case "ipv4", "IPv4": - return apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_V4.Enum() - case "ipv6", "IPv6": - return apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_V6.Enum() - default: - return apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_UNSPECIFIED.Enum() - } -} diff --git a/cmd/api/v2/machine.go b/cmd/api/v2/machine.go new file mode 100644 index 0000000..07d84e6 --- /dev/null +++ b/cmd/api/v2/machine.go @@ -0,0 +1,540 @@ +package v2 + +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/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/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", + Plural: "machines", + Description: "an machine of metal-stack.io", + Sorter: sorters.MachineSorter(), + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + CreateRequestFromCLI: w.createRequestFromCLI, + CreateCmdMutateFn: func(cmd *cobra.Command) { + w.addMachineCreateFlags(cmd, "machine") + 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. +` + }, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project from where machines should be listed") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + }, + DescribeCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("project", "p", "", "project of the machine") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + }, + ValidArgsFn: c.Completion.MachineListCompletion, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *machine) Create(rq *apiv2.MachineServiceCreateRequest) (*apiv2.Machine, error) { + var ( + keys []string + dnsServers []*apiv2.DNSServer + ntpServers []*apiv2.NTPServer + allocationType apiv2.MachineAllocationType + firewallSpec *apiv2.FirewallSpec + labels *apiv2.Labels + ) + + sshPublicKeyArgument := viper.GetString("sshpublickey") + dnsServersArgument := viper.GetStringSlice("dnsservers") + ntpServersArgument := viper.GetStringSlice("ntpservers") + + if strings.HasPrefix(sshPublicKeyArgument, "@") { + var err error + sshPublicKeyArgument, err = readFromFile(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(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(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 + } + + for k, v := range viper.GetStringMap("labels") { + if labels == nil { + labels = &apiv2.Labels{} + } else { + value, ok := v.(string) + if ok { + labels.Labels[k] = value + } else { + labels.Labels[k] = "" + } + } + } + + var filesystemlayout *string + if viper.IsSet("filesystemlayout") { + filesystemlayout = new(viper.GetString("filesystemlayout")) + } + var size *string + if viper.IsSet("size") { + size = new(viper.GetString("size")) + } + var uuid *string + if viper.IsSet("id") { + uuid = new(viper.GetString("id")) + } + 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")) + } + + mcr := &apiv2.MachineServiceCreateRequest{ + Description: description, + Partition: partition, + Hostname: hostname, + Image: viper.GetString("image"), + Name: viper.GetString("name"), + Uuid: uuid, + 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, + } + ctx, cancel := c.c.NewRequestContext() + defer cancel() + resp, err := c.c.Client.Apiv2().Machine().Create(ctx, mcr) + if err != nil { + return nil, fmt.Errorf("unable to create machine:%w", err) + } + return resp.Machine, nil +} + +func (c *machine) createRequestFromCLI() (*apiv2.MachineServiceCreateRequest, error) { + var ( + keys []string + dnsServers []*apiv2.DNSServer + ntpServers []*apiv2.NTPServer + allocationType apiv2.MachineAllocationType + firewallSpec *apiv2.FirewallSpec + labels *apiv2.Labels + ) + + sshPublicKeyArgument := viper.GetString("sshpublickey") + dnsServersArgument := viper.GetStringSlice("dnsservers") + ntpServersArgument := viper.GetStringSlice("ntpservers") + + if strings.HasPrefix(sshPublicKeyArgument, "@") { + var err error + sshPublicKeyArgument, err = readFromFile(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(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(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 + } + + for k, v := range viper.GetStringMap("labels") { + if labels == nil { + labels = &apiv2.Labels{} + } else { + value, ok := v.(string) + if ok { + labels.Labels[k] = value + } else { + labels.Labels[k] = "" + } + } + } + + var filesystemlayout *string + if viper.IsSet("filesystemlayout") { + filesystemlayout = new(viper.GetString("filesystemlayout")) + } + var size *string + if viper.IsSet("size") { + size = new(viper.GetString("size")) + } + var uuid *string + if viper.IsSet("id") { + uuid = new(viper.GetString("id")) + } + 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")) + } + + mcr := &apiv2.MachineServiceCreateRequest{ + Description: description, + Partition: partition, + Hostname: hostname, + Image: viper.GetString("image"), + Name: viper.GetString("name"), + Uuid: uuid, + 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, + } + return mcr, nil +} + +func (c *machine) Delete(id string) (*apiv2.Machine, error) { + panic("unimplemented") +} + +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() + + resp, err := c.c.Client.Apiv2().Machine().List(ctx, &apiv2.MachineServiceListRequest{ + Project: c.c.GetProject(), + Query: &apiv2.MachineQuery{ + // FIXME implement + }, + }) + if err != nil { + return nil, err + } + + return resp.Machines, nil +} + +func (c *machine) Update(rq *apiv2.MachineServiceUpdateRequest) (*apiv2.Machine, error) { + panic("unimplemented") +} + +func (c *machine) Convert(r *apiv2.Machine) (string, *apiv2.MachineServiceCreateRequest, *apiv2.MachineServiceUpdateRequest, error) { + responseToUpdate, err := c.MachineResponseToUpdate(r) + return helpers.EncodeProject(r.Uuid, r.Allocation.Project), c.MachineResponseToCreate(r), responseToUpdate, err +} + +func (c *machine) MachineResponseToCreate(r *apiv2.Machine) *apiv2.MachineServiceCreateRequest { + return &apiv2.MachineServiceCreateRequest{ + // FIXME + } +} + +func (c *machine) MachineResponseToUpdate(desired *apiv2.Machine) (*apiv2.MachineServiceUpdateRequest, error) { + panic("unimplemented") +} + +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(filePath string) (string, error) { + filePath, err := expandFilepath(filePath) + if err != nil { + return "", err + } + + content, err := os.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("unable to read from given file %s error:%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:%s %w", ip, err) + } + man.Ips = append(man.Ips, ip) + } + } + result = append(result, man) + } + return result, nil +} + +func (c *machine) addMachineCreateFlags(cmd *cobra.Command, name string) { + cmd.Flags().StringP("description", "d", "", "Description of the "+name+" to create. [optional]") + cmd.Flags().StringP("partition", "S", "", "partition/datacenter where the "+name+" is created. [required, except for reserved machines]") + cmd.Flags().StringP("hostname", "H", "", "Hostname of the "+name+". [required]") + cmd.Flags().StringP("image", "i", "", "OS Image to install. [required]") + cmd.Flags().StringP("filesystemlayout", "", "", "Filesystemlayout to use during machine installation. [optional]") + cmd.Flags().StringP("name", "n", "", "Name of the "+name+". [optional]") + cmd.Flags().StringP("id", "I", "", "ID of a specific "+name+" to allocate, if given, size and partition are ignored. Need to be set to reserved (--reserve) state before.") + cmd.Flags().StringP("project", "P", "", "Project where the "+name+" should belong to. [required]") + cmd.Flags().StringP("size", "s", "", "Size of the "+name+". [required, except for reserved machines]") + cmd.Flags().StringP("allocation-type", "t", "machine", "allocation type, can be either machine|firewall") + cmd.Flags().StringP("sshpublickey", "p", "", + `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("tags", []string{}, "tags to add to the "+name+", use it like: --tags \"tag1,tag2\" or --tags \"tag3\".") + cmd.Flags().StringP("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("dnsservers", []string{}, "dns servers to add to the machine or firewall. [optional]") + cmd.Flags().StringSlice("ntpservers", []string{}, "ntp servers to add to the machine or firewall. [optional]") + + cmd.Flags().StringSlice("networks", []string{}, + `Adds a network. Usage: [--networks NETWORK,[ip:ip:ip][,NETWORK...]... +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.MarkFlagsMutuallyExclusive("file", "project") + cmd.MarkFlagsRequiredTogether("project", "networks", "hostname", "image") + cmd.MarkFlagsRequiredTogether("size", "partition") + + // Completion for arguments + genericcli.Must(cmd.RegisterFlagCompletionFunc("networks", c.c.Completion.NetworkListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.c.Completion.PartitionListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.c.Completion.SizeListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.c.Completion.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("id", c.c.Completion.MachineListCompletion)) + // FIXME implement + // genericcli.Must(cmd.RegisterFlagCompletionFunc("image", c.c.Completion.ImageListCompletion)) + // genericcli.Must(cmd.RegisterFlagCompletionFunc("filesystemlayout", c.c.Completion.FilesystemLayoutListCompletion)) +} diff --git a/cmd/api/v2/network.go b/cmd/api/v2/network.go new file mode 100644 index 0000000..8d80a32 --- /dev/null +++ b/cmd/api/v2/network.go @@ -0,0 +1,320 @@ +package v2 + +import ( + "github.com/metal-stack/api/go/enum" + 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/common" + "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/metal-stack/metal-lib/pkg/tag" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type networkCmd struct { + c *config.Config +} + +func newNetworkCmd(c *config.Config) *cobra.Command { + w := &networkCmd{ + c: c, + } + + listFlags := func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "ID to filter [optional]") + cmd.Flags().String("name", "", "name to filter [optional]") + cmd.Flags().String("description", "", "description to filter [optional]") + cmd.Flags().String("partition", "", "partition to filter [optional]") + cmd.Flags().String("project", "", "project to filter [optional]") + cmd.Flags().StringSlice("prefixes", []string{}, "prefixes to filter") + cmd.Flags().StringSlice("destination-prefixes", []string{}, "destination prefixes to filter") + cmd.Flags().String("addressfamily", "", "addressfamily to filter, either ipv4 or ipv6 [optional]") + cmd.Flags().Uint32("vrf", 0, "vrf to filter [optional]") + cmd.Flags().StringSlice("labels", nil, "labels to filter [optional]") + cmd.Flags().StringP("type", "t", "", "type of the network. [optional]") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.PartitionListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("addressfamily", c.Completion.NetworkAddressFamilyCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("type", c.Completion.NetworkTypeCompletion)) + } + + cmdsConfig := &genericcli.CmdsConfig[*apiv2.NetworkServiceCreateRequest, *apiv2.NetworkServiceUpdateRequest, *apiv2.Network]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "network", + Plural: "networks", + Description: "networks can be attached to a machine or firewall such that they can communicate with each other.", + CreateRequestFromCLI: w.createRequestFromCLI, + UpdateRequestFromCLI: w.updateRequestFromCLI, + Sorter: sorters.NetworkSorter(), + ValidArgsFn: c.Completion.NetworkListCompletion, + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("name", "", "name of the network to create. [required]") + cmd.Flags().String("partition", "", "partition where this network should exist. [required]") + cmd.Flags().String("project", "", "partition where this network should exist (alternative to parent-network-id). [optional]") + cmd.Flags().String("parent-network-id", "", "the parent of the network (alternative to partition). [optional]") + cmd.Flags().String("description", "", "description of the network to create. [optional]") + cmd.Flags().StringSlice("labels", nil, "labels for this network. [optional]") + cmd.Flags().String("addressfamily", "", "addressfamily of the network to acquire, if not specified the network inherits the address families from the parent [optional]") + cmd.Flags().Uint32("ipv4-prefix-length", 0, "ipv4 prefix bit length of the network to create, defaults to default child prefix length of the parent network. [optional]") + cmd.Flags().Uint32("ipv6-prefix-length", 0, "ipv6 prefix bit length of the network to create, defaults to default child prefix length of the parent network. [optional]") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.PartitionListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("addressfamily", c.Completion.NetworkAddressFamilyCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("parent-network-id", c.Completion.NetworkListCompletion)) + }, + ListCmdMutateFn: func(cmd *cobra.Command) { + listFlags(cmd) + cmd.Flags().String("parent-network-id", "", "parent network to filter [optional]") + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("name", "", "the name of the network [optional]") + cmd.Flags().String("description", "", "the description of the network [optional]") + cmd.Flags().StringSlice("labels", nil, "the labels of the network, must be in the form of key=value, use it like: --labels \"key1=value1,key2=value2\". [optional]") + cmd.Flags().String("project", "", "project to filter [optional]") + }, + } + + listBaseNetworksCmd := &cobra.Command{ + Use: "list-base-networks", + Short: "lists base networks that can be used for network creation", + RunE: func(cmd *cobra.Command, _ []string) error { + return w.listBaseNetworks() + }, + ValidArgsFunction: c.Completion.TenantMemberListCompletion, + } + listFlags(listBaseNetworksCmd) + + return genericcli.NewCmds(cmdsConfig, listBaseNetworksCmd) +} + +func (c *networkCmd) Get(id string) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Network().Get(ctx, &apiv2.NetworkServiceGetRequest{ + Id: id, + Project: c.c.GetProject(), + }) + if err != nil { + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) List() ([]*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Network().List(ctx, &apiv2.NetworkServiceListRequest{ + Project: c.c.GetProject(), + Query: &apiv2.NetworkQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Project: new(c.c.GetProject()), + Prefixes: viper.GetStringSlice("prefixes"), + DestinationPrefixes: viper.GetStringSlice("destination-prefixes"), + Vrf: pointer.PointerOrNil(viper.GetUint32("vrf")), + ParentNetwork: pointer.PointerOrNil(viper.GetString("parent-network-id")), + AddressFamily: common.NetworkAddressFamilyToType(viper.GetString("addressfamily")), + Labels: &apiv2.Labels{ + Labels: tag.NewTagMap(viper.GetStringSlice("labels")), + }, + }, + }) + + if err != nil { + return nil, err + } + + return resp.Networks, nil +} + +func (c *networkCmd) Delete(id string) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Network().Delete(ctx, &apiv2.NetworkServiceDeleteRequest{ + Id: id, + Project: c.c.GetProject(), + }) + if err != nil { + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) Create(rq *apiv2.NetworkServiceCreateRequest) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Network().Create(ctx, rq) + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.AlreadyExists { + return nil, genericcli.AlreadyExistsError() + } + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) Update(rq *apiv2.NetworkServiceUpdateRequest) (*apiv2.Network, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Apiv2().Network().Update(ctx, rq) + if err != nil { + return nil, err + } + + return resp.Network, nil +} + +func (c *networkCmd) Convert(r *apiv2.Network) (string, *apiv2.NetworkServiceCreateRequest, *apiv2.NetworkServiceUpdateRequest, error) { + return r.Id, networkResponseToCreate(r), networkResponseToUpdate(r), nil +} + +func networkResponseToCreate(r *apiv2.Network) *apiv2.NetworkServiceCreateRequest { + meta := pointer.SafeDeref(r.Meta) + + return &apiv2.NetworkServiceCreateRequest{ + Project: pointer.SafeDeref(r.Project), + Name: r.Name, + Description: r.Description, + Partition: r.Partition, + Labels: &apiv2.Labels{ + Labels: pointer.SafeDeref(meta.Labels).Labels, + }, + ParentNetwork: r.ParentNetwork, + // TODO: allow defining length and addressfamilies somehow? + } +} + +func networkResponseToUpdate(r *apiv2.Network) *apiv2.NetworkServiceUpdateRequest { + meta := pointer.SafeDeref(r.Meta) + + return &apiv2.NetworkServiceUpdateRequest{ + Id: r.Id, + Project: pointer.SafeDeref(r.Project), + Name: r.Name, + Description: r.Description, + Labels: &apiv2.UpdateLabels{ + Update: meta.Labels, // TODO: this only ensures that the labels are present but it does not cleanup old one's, which would require fetching the current state and calculating the diff + }} +} + +func (c *networkCmd) createRequestFromCLI() (*apiv2.NetworkServiceCreateRequest, error) { + labels, err := genericcli.LabelsToMap(viper.GetStringSlice("labels")) + if err != nil { + return nil, err + } + + var ( + cpl = &apiv2.ChildPrefixLength{} + ) + if viper.IsSet("ipv4-prefix-length") { + cpl.Ipv4 = new(viper.GetUint32("ipv4-prefix-length")) + } + if viper.IsSet("ipv6-prefix-length") { + cpl.Ipv6 = new(viper.GetUint32("ipv6-prefix-length")) + } + + return &apiv2.NetworkServiceCreateRequest{ + Description: pointer.PointerOrNil(viper.GetString("description")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Project: c.c.GetProject(), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Labels: &apiv2.Labels{ + Labels: labels, + }, + ParentNetwork: pointer.PointerOrNil(viper.GetString("parent-network-id")), + Length: cpl, + AddressFamily: common.NetworkAddressFamilyToType(viper.GetString("addressfamily")), + }, nil +} + +func (c *networkCmd) updateRequestFromCLI(args []string) (*apiv2.NetworkServiceUpdateRequest, error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return nil, err + } + + var labels *apiv2.UpdateLabels + if viper.IsSet("labels") { + lbls, err := genericcli.LabelsToMap(viper.GetStringSlice("labels")) + if err != nil { + return nil, err + } + + labels = &apiv2.UpdateLabels{ + Update: &apiv2.Labels{ + Labels: lbls, + }, + } + } + + var ( + ur = &apiv2.NetworkServiceUpdateRequest{ + Id: id, + Project: c.c.GetProject(), + Description: pointer.PointerOrNil(viper.GetString("description")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Labels: labels, + } + ) + + return ur, nil +} + +func (c *networkCmd) listBaseNetworks() error { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + var nwType *apiv2.NetworkType + if viper.IsSet("type") { + nt, err := enum.GetEnum[apiv2.NetworkType](viper.GetString("type")) + if err != nil { + return err + } + nwType = &nt + } + + resp, err := c.c.Client.Apiv2().Network().ListBaseNetworks(ctx, &apiv2.NetworkServiceListBaseNetworksRequest{ + Project: c.c.GetProject(), + Query: &apiv2.NetworkQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partition: pointer.PointerOrNil(viper.GetString("partition")), + Project: pointer.PointerOrNil(viper.GetString("project")), + Prefixes: viper.GetStringSlice("prefixes"), + DestinationPrefixes: viper.GetStringSlice("destination-prefixes"), + Vrf: pointer.PointerOrNil(viper.GetUint32("vrf")), + AddressFamily: common.NetworkAddressFamilyToType(viper.GetString("addressfamily")), + Labels: &apiv2.Labels{ + Labels: tag.NewTagMap(viper.GetStringSlice("labels")), + }, + Type: nwType, + }, + }) + + if err != nil { + return err + } + + return c.c.ListPrinter.Print(resp.Networks) +} diff --git a/cmd/api/v2/size.go b/cmd/api/v2/size.go new file mode 100644 index 0000000..dc6cd13 --- /dev/null +++ b/cmd/api/v2/size.go @@ -0,0 +1,92 @@ +package v2 + +import ( + "fmt" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" + "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 size struct { + c *config.Config +} + +func newSizeCmd(c *config.Config) *cobra.Command { + w := &size{ + c: c, + } + + gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) + + cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Size]{ + BinaryName: config.BinaryName, + GenericCLI: gcli, + Singular: "size", + Plural: "sizes", + Description: "manage sizes which defines the cpu, gpu, memory and storage properties of machines", + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ValidArgsFn: c.Completion.SizeListCompletion, + OnlyCmds: genericcli.OnlyCmds(genericcli.DescribeCmd, genericcli.ListCmd), + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("id", "", "", "size id to filter for") + cmd.Flags().StringP("name", "", "", "size name to filter for") + cmd.Flags().StringP("description", "", "", "size description to filter for") + }, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *size) Get(id string) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceGetRequest{Id: id} + + resp, err := c.c.Client.Apiv2().Size().Get(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) List() ([]*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceListRequest{Query: &apiv2.SizeQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + }} + + resp, err := c.c.Client.Apiv2().Size().List(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get sizes: %w", err) + } + + return resp.Sizes, nil +} + +func (c *size) Create(rq any) (*apiv2.Size, error) { + panic("unimplemented") +} + +func (c *size) Delete(id string) (*apiv2.Size, error) { + panic("unimplemented") +} + +func (t *size) Convert(r *apiv2.Size) (string, any, any, error) { + panic("unimplemented") +} + +func (t *size) Update(rq any) (*apiv2.Size, error) { + panic("unimplemented") +} diff --git a/cmd/api/v2/token.go b/cmd/api/v2/token.go index 7bcae98..ff281c6 100644 --- a/cmd/api/v2/token.go +++ b/cmd/api/v2/token.go @@ -30,7 +30,7 @@ func newTokenCmd(c *config.Config) *cobra.Command { GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "token", Plural: "tokens", - Description: "manage api tokens for accessing the metal-stack.io api", + Description: "manage api tokens", Sorter: sorters.TokenSorter(), DescribePrinter: func() printers.Printer { return c.DescribePrinter }, ListPrinter: func() printers.Printer { return c.ListPrinter }, @@ -80,7 +80,8 @@ func newTokenCmd(c *config.Config) *cobra.Command { var adminRole *apiv2.AdminRole if roleString := viper.GetString("admin-role"); roleString != "" { - role, ok := apiv2.AdminRole_value[roleString] + // FIXME see: https://github.com/golangci/golangci-lint/issues/6363 + role, ok := apiv2.AdminRole_value[roleString] // nolint:staticcheck if !ok { return nil, fmt.Errorf("unknown role: %s", roleString) } diff --git a/cmd/completion/ip.go b/cmd/completion/ip.go index b433db5..148e780 100644 --- a/cmd/completion/ip.go +++ b/cmd/completion/ip.go @@ -1,6 +1,7 @@ package completion import ( + "github.com/metal-stack/api/go/enum" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/spf13/cobra" ) @@ -19,3 +20,32 @@ func (c *Completion) IpListCompletion(cmd *cobra.Command, args []string, toCompl } return names, cobra.ShellCompDirectiveNoFileComp } + +func (c *Completion) IpAddressFamilyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var afs []string + for _, af := range []apiv2.IPAddressFamily{ + apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_V4, + apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_V6} { + stringValue, err := enum.GetStringValue(af) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + afs = append(afs, *stringValue) + } + + return afs, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) IpTypeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var afs []string + for _, af := range []apiv2.IPType{ + apiv2.IPType_IP_TYPE_EPHEMERAL, + apiv2.IPType_IP_TYPE_STATIC} { + stringValue, err := enum.GetStringValue(af) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + afs = append(afs, *stringValue) + } + + return afs, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/completion/machine.go b/cmd/completion/machine.go new file mode 100644 index 0000000..d54f93d --- /dev/null +++ b/cmd/completion/machine.go @@ -0,0 +1,35 @@ +package completion + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/spf13/cobra" +) + +func (c *Completion) MachineListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + req := &apiv2.MachineServiceListRequest{ + Project: c.Project, + } + resp, err := c.Client.Apiv2().Machine().List(c.Ctx, req) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, s := range resp.Machines { + names = append(names, s.Uuid) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) BMCCommandListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{ + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_ON.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_OFF.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_RESET.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_BOOT_FROM_DISK.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_BOOT_FROM_PXE.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_BOOT_TO_BIOS.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_CYCLE.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_IDENTIFY_LED_OFF.String(), + apiv2.MachineBMCCommand_MACHINE_BMC_COMMAND_IDENTIFY_LED_ON.String(), + }, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/completion/network.go b/cmd/completion/network.go new file mode 100644 index 0000000..962ce64 --- /dev/null +++ b/cmd/completion/network.go @@ -0,0 +1,85 @@ +package completion + +import ( + "github.com/metal-stack/api/go/enum" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/spf13/cobra" +) + +func (c *Completion) NetworkListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ownNetworks, err := c.Client.Apiv2().Network().List(c.Ctx, &apiv2.NetworkServiceListRequest{ + Project: c.Project, + }) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + baseNetworks, err := c.Client.Apiv2().Network().ListBaseNetworks(c.Ctx, &apiv2.NetworkServiceListBaseNetworksRequest{ + Project: c.Project, + }) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + for _, s := range baseNetworks.Networks { + names = append(names, s.Id+"\t"+pointer.SafeDeref(s.Name)) + } + for _, s := range ownNetworks.Networks { + names = append(names, s.Id+"\t"+pointer.SafeDeref(s.Name)) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) NetworkTypeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var names []string + for _, val := range apiv2.NetworkType_value { + if e, err := enum.GetStringValue(apiv2.NetworkType(val)); err == nil { + names = append(names, *e) + } + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) NetworkNatTypeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var names []string + for _, val := range apiv2.NATType_value { + if e, err := enum.GetStringValue(apiv2.NATType(val)); err == nil { + names = append(names, *e) + } + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) NetworkAddressFamilyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var afs []string + for _, af := range []apiv2.NetworkAddressFamily{ + apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_DUAL_STACK, + apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4, + apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6} { + stringValue, err := enum.GetStringValue(af) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + afs = append(afs, *stringValue) + } + + return afs, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) NetworkAdminListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + networks, err := c.Client.Adminv2().Network().List(c.Ctx, &adminv2.NetworkServiceListRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + for _, s := range networks.Networks { + names = append(names, s.Id+"\t"+pointer.SafeDeref(s.Name)) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/completion/partition.go b/cmd/completion/partition.go new file mode 100644 index 0000000..f3b0c26 --- /dev/null +++ b/cmd/completion/partition.go @@ -0,0 +1,20 @@ +package completion + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/spf13/cobra" +) + +func (c *Completion) PartitionListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.Client.Apiv2().Partition().List(c.Ctx, &apiv2.PartitionServiceListRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var names []string + for _, s := range resp.Partitions { + names = append(names, s.Id+"\t"+s.Description) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/completion/size.go b/cmd/completion/size.go new file mode 100644 index 0000000..8ebd814 --- /dev/null +++ b/cmd/completion/size.go @@ -0,0 +1,19 @@ +package completion + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/spf13/cobra" +) + +func (c *Completion) SizeListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + req := &apiv2.SizeServiceListRequest{} + resp, err := c.Client.Apiv2().Size().List(c.Ctx, req) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, s := range resp.Sizes { + names = append(names, s.Id) + } + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/completion/token.go b/cmd/completion/token.go index e23fbb2..4f9d747 100644 --- a/cmd/completion/token.go +++ b/cmd/completion/token.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/api/go/permissions" ) func (c *Completion) TokenListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -64,11 +65,8 @@ func (c *Completion) TokenAdminRoleCompletion(cmd *cobra.Command, args []string, } func (c *Completion) TokenPermissionsCompletionfunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - methods, err := c.Client.Apiv2().Method().TokenScopedList(c.Ctx, &apiv2.MethodServiceTokenScopedListRequest{}) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } + methods := permissions.GetServicePermissions().Methods subject := "" if s, _, ok := strings.Cut(toComplete, "="); ok { subject = s @@ -77,19 +75,17 @@ func (c *Completion) TokenPermissionsCompletionfunc(cmd *cobra.Command, args []s if subject == "" { var perms []string - for _, p := range methods.Permissions { - perms = append(perms, p.Subject) + for p := range methods { + perms = append(perms, p) } return perms, cobra.ShellCompDirectiveNoFileComp } - // FIXME: completion does not work at this point, investigate why - var perms []string - for _, p := range methods.Permissions { - perms = append(perms, p.Methods...) + for p := range methods { + perms = append(perms, p) } return perms, cobra.ShellCompDirectiveDefault diff --git a/cmd/sorters/machine.go b/cmd/sorters/machine.go new file mode 100644 index 0000000..e1ba415 --- /dev/null +++ b/cmd/sorters/machine.go @@ -0,0 +1,33 @@ +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(a.Partition.Id, b.Partition.Id, descending) + }, + "size": func(a, b *apiv2.Machine, descending bool) multisort.CompareResult { + return multisort.Compare(a.Size.Id, 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(a.Allocation).Image.Id, 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(pointer.SafeDeref(a.Allocation).Meta.CreatedAt.AsTime().Unix(), pointer.SafeDeref(b.Allocation).Meta.CreatedAt.AsTime().Unix(), descending) + }, + }, multisort.Keys{{ID: "uuid"}, {ID: "size"}, {ID: "partition"}}) +} diff --git a/cmd/sorters/network.go b/cmd/sorters/network.go new file mode 100644 index 0000000..a490f7b --- /dev/null +++ b/cmd/sorters/network.go @@ -0,0 +1,27 @@ +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 NetworkSorter() *multisort.Sorter[*apiv2.Network] { + return multisort.New(multisort.FieldMap[*apiv2.Network]{ + "id": func(a, b *apiv2.Network, descending bool) multisort.CompareResult { + return multisort.Compare(a.Id, b.Id, descending) + }, + "name": func(a, b *apiv2.Network, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Name), pointer.SafeDeref(b.Name), descending) + }, + "description": func(a, b *apiv2.Network, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Description), pointer.SafeDeref(b.Description), descending) + }, + "partition": func(a, b *apiv2.Network, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Partition), pointer.SafeDeref(b.Partition), descending) + }, + "project": func(a, b *apiv2.Network, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Project), pointer.SafeDeref(b.Project), descending) + }, + }, multisort.Keys{{ID: "partition"}, {ID: "id"}}) +} diff --git a/cmd/tableprinters/common.go b/cmd/tableprinters/common.go index 933e6f9..8c79f1f 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -14,17 +14,20 @@ import ( ) const ( - dot = "●" - nbr = " " - - ambulance = "🚑" - exclamation = "❗" - bark = "🚧" - loop = "⭕" - lock = "🔒" - question = "❓" - skull = "💀" - vpn = "🛡" + dot = "●" + halfpie = "◒" + threequarterpie = "◕" + nbr = " " + poweron = "⏻" + powersleep = "⏾" + ambulance = "🚑" + exclamation = "❗" + bark = "🚧" + loop = "⭕" + lock = "🔒" + question = "❓" + skull = "💀" + vpn = "🛡" ) type TablePrinter struct { @@ -36,14 +39,14 @@ func New() *TablePrinter { return &TablePrinter{} } -func (t *TablePrinter) SetPrinter(printer *printers.TablePrinter) { - t.t = printer -} - func (t *TablePrinter) SetLastEventErrorThreshold(threshold time.Duration) { t.lastEventErrorThreshold = threshold } +func (t *TablePrinter) SetPrinter(printer *printers.TablePrinter) { + t.t = printer +} + func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]string, error) { switch d := data.(type) { @@ -70,6 +73,21 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case []*apiv2.Image: return t.ImageTable(d, wide) + case *apiv2.Size: + return t.SizeTable(pointer.WrapInSlice(d), wide) + case []*apiv2.Size: + return t.SizeTable(d, wide) + + case *apiv2.Machine: + return t.MachineTable(pointer.WrapInSlice(d), wide) + case []*apiv2.Machine: + return t.MachineTable(d, wide) + + case *apiv2.Network: + return t.NetworkTable(pointer.WrapInSlice(d), wide) + case []*apiv2.Network: + return t.NetworkTable(d, wide) + case *apiv2.Project: return t.ProjectTable(pointer.WrapInSlice(d), wide) case []*apiv2.Project: @@ -95,6 +113,11 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case []*apiv2.Token: return t.TokenTable(d, wide) + case *apiv2.VPNNode: + return t.VPNTable(pointer.WrapInSlice(d), wide) + case []*apiv2.VPNNode: + return t.VPNTable(d, wide) + case *apiv2.Tenant: return t.TenantTable(pointer.WrapInSlice(d), wide) case []*apiv2.Tenant: @@ -165,3 +188,14 @@ func humanizeDuration(duration time.Duration) string { } return strings.Join(parts, " ") } + +func getMaxLineCount(ss ...string) int { + max := 0 + for _, s := range ss { + c := strings.Count(s, "\n") + if c > max { + max = c + } + } + return max +} diff --git a/cmd/tableprinters/ip.go b/cmd/tableprinters/ip.go index d60a149..85764a2 100644 --- a/cmd/tableprinters/ip.go +++ b/cmd/tableprinters/ip.go @@ -5,16 +5,17 @@ import ( "strings" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/pointer" ) func (t *TablePrinter) IPTable(data []*apiv2.IP, wide bool) ([]string, [][]string, error) { var ( rows [][]string - header = []string{"IP", "Project", "ID", "Type", "Name", "Attached Service"} + header = []string{"IP", "NS", "Project", "ID", "Type", "Name", "Attached Service"} ) if wide { - header = []string{"IP", "Project", "ID", "Type", "Name", "Description", "Labels"} + header = []string{"IP", "NS", "Project", "ID", "Type", "Name", "Description", "Labels"} } for _, ip := range data { @@ -40,11 +41,12 @@ func (t *TablePrinter) IPTable(data []*apiv2.IP, wide bool) ([]string, [][]strin labels = append(labels, fmt.Sprintf("%s=%s", k, v)) } } + ns := pointer.SafeDeref(ip.Namespace) if wide { - rows = append(rows, []string{ip.Ip, ip.Project, ip.Uuid, t, ip.Name, ip.Description, strings.Join(labels, "\n")}) + rows = append(rows, []string{ip.Ip, ns, ip.Project, ip.Uuid, t, ip.Name, ip.Description, strings.Join(labels, "\n")}) } else { - rows = append(rows, []string{ip.Ip, ip.Project, ip.Uuid, t, ip.Name, attachedService}) + rows = append(rows, []string{ip.Ip, ns, ip.Project, ip.Uuid, t, ip.Name, attachedService}) } } diff --git a/cmd/tableprinters/machine.go b/cmd/tableprinters/machine.go new file mode 100644 index 0000000..5c40a47 --- /dev/null +++ b/cmd/tableprinters/machine.go @@ -0,0 +1,165 @@ +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", "Lock/Reserve"} + } + 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) + } + + 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) + + var nwIPs []string + for _, nw := range alloc.Networks { + nwIPs = append(nwIPs, nw.Ips...) + } + ips := strings.Join(nwIPs, "\n") + + started := "" + age := "" + + 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())) + } + tags := "" + 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, ",") + } + + reserved := "" + if machine.Status.Condition != nil { + stateString, err := enum.GetStringValue(machine.Status.Condition.State) + if err != nil { + return nil, nil, err + } + reserved = fmt.Sprintf("%s:%s", *stateString, machine.Status.Condition.Description) + } + + lastEvent := "" + when := "" + 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.Status.Liveliness, machine.RecentProvisioningEvents, machine.Status.Condition.State, alloc.Vpn) + + 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) getMachineStatusEmojis(liveliness apiv2.MachineLiveliness, events *apiv2.MachineRecentProvisioningEvents, state apiv2.MachineState, vpn *apiv2.MachineVPN) (string, string) { + var ( + emojis []string + wide []string + livelinessString *string + err error + ) + livelinessString, err = enum.GetStringValue(liveliness) + if err != nil { + livelinessString = new("unknown") + } + + switch l := liveliness; l { + case apiv2.MachineLiveliness_MACHINE_LIVELINESS_ALIVE: + // noop + case apiv2.MachineLiveliness_MACHINE_LIVELINESS_DEAD: + emojis = append(emojis, helpers.Skull) + wide = append(wide, *livelinessString) + case apiv2.MachineLiveliness_MACHINE_LIVELINESS_UNKNOWN: + emojis = append(emojis, helpers.Question) + wide = append(wide, *livelinessString) + default: + emojis = append(emojis, helpers.Question) + wide = append(wide, *livelinessString) + } + + switch state { + case apiv2.MachineState_MACHINE_STATE_AVAILABLE: + // noop + case apiv2.MachineState_MACHINE_STATE_LOCKED: + emojis = append(emojis, helpers.Lock) + wide = append(wide, "Locked") + case apiv2.MachineState_MACHINE_STATE_TAINTED: + emojis = append(emojis, helpers.Bark) + wide = append(wide, "Tainted") + } + + if events != nil { + switch events.State { + case apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_FAILED_RECLAIM: + emojis = append(emojis, helpers.Ambulance) + wide = append(wide, "FailedReclaim") + case apiv2.MachineProvisioningEventState_MACHINE_PROVISIONING_EVENT_STATE_CRASHLOOP: + emojis = append(emojis, helpers.Loop) + wide = append(wide, "CrashLoop") + + } + + if events.LastErrorEvent != nil && time.Since(events.LastErrorEvent.Time.AsTime()) < t.lastEventErrorThreshold { + emojis = append(emojis, helpers.Exclamation) + wide = append(wide, "LastEventErrors") + } + + } + + if vpn != nil && vpn.Connected { + emojis = append(emojis, helpers.VPN) + wide = append(wide, "VPN") + } + + return strings.Join(emojis, nbr), strings.Join(wide, ", ") +} diff --git a/cmd/tableprinters/network.go b/cmd/tableprinters/network.go new file mode 100644 index 0000000..f368fe5 --- /dev/null +++ b/cmd/tableprinters/network.go @@ -0,0 +1,156 @@ +package tableprinters + +import ( + "fmt" + "strings" + + "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/metal-lib/pkg/pointer" +) + +type network struct { + parent *apiv2.Network + children []*apiv2.Network +} + +type networks []*network + +func (t *TablePrinter) NetworkTable(data []*apiv2.Network, wide bool) ([]string, [][]string, error) { + var ( + rows [][]string + ) + + header := []string{"ID", "Name", "Type", "Project", "Partition", "Nat", "Prefixes", "Prefix Usage", "IP Usage"} + if wide { + header = []string{"ID", "Description", "Name", "Type", "Project", "Partition", "Nat", "Prefixes", "Annotations"} + } + + nn := &networks{} + for _, n := range data { + if n.ParentNetwork == nil { + *nn = append(*nn, &network{parent: n}) + } + } + for _, n := range data { + if n.ParentNetwork != nil { + if !nn.appendChild(*n.ParentNetwork, n) { + *nn = append(*nn, &network{parent: n}) + } + } + } + for _, n := range *nn { + rows = append(rows, addNetwork("", n.parent, wide)) + for i, c := range n.children { + prefix := "├" + if i == len(n.children)-1 { + prefix = "└" + } + prefix += "─╴" + rows = append(rows, addNetwork(prefix, c, wide)) + } + } + + return header, rows, nil +} + +func addNetwork(prefix string, n *apiv2.Network, wide bool) []string { + id := fmt.Sprintf("%s%s", prefix, n.Id) + + prefixes := strings.Join(n.Prefixes, ",") + shortIPUsage := nbr + shortPrefixUsage := nbr + ipv4Use := 0.0 + ipv4PrefixUse := 0.0 + ipv6Use := 0.0 + ipv6PrefixUse := 0.0 + + if n.Consumption != nil { + consumption := n.Consumption + if consumption.Ipv4 != nil { + ipv4Consumption := consumption.Ipv4 + ipv4Use = float64(ipv4Consumption.UsedIps) / float64(ipv4Consumption.AvailableIps) + + if ipv4Consumption.AvailablePrefixes > 0 { + ipv4PrefixUse = float64(ipv4Consumption.UsedPrefixes) / float64(ipv4Consumption.AvailablePrefixes) + } + } + if consumption.Ipv6 != nil { + ipv6Consumption := consumption.Ipv6 + ipv6Use = float64(ipv6Consumption.UsedIps) / float64(ipv6Consumption.AvailableIps) + + if ipv6Consumption.AvailablePrefixes > 0 { + ipv6PrefixUse = float64(ipv6Consumption.UsedPrefixes) / float64(ipv6Consumption.AvailablePrefixes) + } + } + + if ipv4Use >= 0.9 || ipv6Use >= 0.9 { + shortIPUsage = color.RedString(threequarterpie) + } else if ipv4Use >= 0.7 || ipv6Use >= 0.7 { + shortIPUsage = color.YellowString(halfpie) + } else { + shortIPUsage = color.GreenString(dot) + } + + if ipv4PrefixUse >= 0.9 || ipv6PrefixUse >= 0.9 { + shortPrefixUsage = color.RedString(threequarterpie) + } else if ipv4PrefixUse >= 0.7 || ipv6PrefixUse >= 0.7 { + shortPrefixUsage = color.YellowString(halfpie) + } else { + shortPrefixUsage = color.GreenString(dot) + } + } + + var ( + description = pointer.SafeDeref(n.Description) + name = pointer.SafeDeref(n.Name) + project = pointer.SafeDeref(n.Project) + partition = pointer.SafeDeref(n.Partition) + natType = n.NatType.String() + ) + + if t, err := enum.GetStringValue(n.NatType); err == nil { + natType = *t + } else { + fmt.Println(err) + } + + max := getMaxLineCount(description, name, project, partition, natType, prefixes, shortIPUsage) + for range max - 1 { + id += "\n│" + } + + var as []string + if n.Meta.Labels != nil { + for k, v := range n.Meta.Labels.Labels { + as = append(as, k+"="+v) + } + } + + annotations := strings.Join(as, "\n") + + var networkType string + nt, err := enum.GetStringValue(n.Type) + if err != nil { + networkType = "unknown" + } else { + networkType = *nt + } + + if wide { + return []string{id, description, name, networkType, project, partition, natType, prefixes, annotations} + } else { + return []string{id, name, networkType, project, partition, natType, prefixes, shortPrefixUsage, shortIPUsage} + } +} + +func (nn *networks) appendChild(parentID string, child *apiv2.Network) bool { + for _, n := range *nn { + if n.parent.Id == parentID { + n.children = append(n.children, child) + return true + } + } + return false +} diff --git a/cmd/tableprinters/size.go b/cmd/tableprinters/size.go new file mode 100644 index 0000000..65f72c2 --- /dev/null +++ b/cmd/tableprinters/size.go @@ -0,0 +1,45 @@ +package tableprinters + +import ( + "fmt" + + "github.com/dustin/go-humanize" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func (t *TablePrinter) SizeTable(data []*apiv2.Size, wide bool) ([]string, [][]string, error) { + var ( + rows [][]string + header = []string{"ID", "Name", "Description", "CPU Range", "Memory Range", "Storage Range", "GPU Range"} + ) + + for _, size := range data { + var ( + cpu string + memory string + storage string + gpu string + ) + + for _, c := range size.Constraints { + switch c.Type { + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_CORES: + cpu = fmt.Sprintf("%d - %d", c.Min, c.Max) + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_MEMORY: + memory = fmt.Sprintf("%s - %s", humanize.Bytes(uint64(c.Min)), humanize.Bytes(uint64(c.Max))) //nolint:gosec + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_STORAGE: + storage = fmt.Sprintf("%s - %s", humanize.Bytes(uint64(c.Min)), humanize.Bytes(uint64(c.Max))) //nolint:gosec + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_GPU: + gpu = fmt.Sprintf("%s: %d - %d", pointer.SafeDeref(c.Identifier), c.Min, c.Max) + } + + } + + rows = append(rows, []string{size.Id, pointer.SafeDeref(size.Name), pointer.SafeDeref(size.Description), cpu, memory, storage, gpu}) + } + + t.t.DisableAutoWrap(false) + + return header, rows, nil +} diff --git a/cmd/tableprinters/switch.go b/cmd/tableprinters/switch.go index f898caa..7ddcef8 100644 --- a/cmd/tableprinters/switch.go +++ b/cmd/tableprinters/switch.go @@ -256,7 +256,12 @@ func (t *TablePrinter) SwitchWithConnectedMachinesTable(res []*apiv2.SwitchWithM } if wide { - rows = append(rows, []string{fmt.Sprintf("%s%s", prefix, machine.Uuid), t.getMachineStatusEmojis(machine), nicName, nicIdentifier, partition, rack, machineSize, allocationHostname, fruProductSerial, fruChassisPartSerial}) + status := &apiv2.MachineStatus{ + Condition: machine.Status.GetCondition(), + Liveliness: machine.Status.GetLiveliness(), + } + emojis, _ := t.getMachineStatusEmojis(status.GetLiveliness(), machine.RecentProvisioningEvents, status.GetCondition().GetState(), machine.Allocation.GetVpn()) + rows = append(rows, []string{fmt.Sprintf("%s%s", prefix, machine.Uuid), emojis, nicName, nicIdentifier, partition, rack, machineSize, allocationHostname, fruProductSerial, fruChassisPartSerial}) } else { rows = append(rows, []string{fmt.Sprintf("%s%s", prefix, machine.Uuid), nicName, nicIdentifier, partition, rack, machineSize, fruProductSerial, fruChassisPartSerial}) } @@ -313,60 +318,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/cmd/tableprinters/vpn.go b/cmd/tableprinters/vpn.go new file mode 100644 index 0000000..0de30db --- /dev/null +++ b/cmd/tableprinters/vpn.go @@ -0,0 +1,35 @@ +package tableprinters + +import ( + "fmt" + "strings" + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" +) + +func (t *TablePrinter) VPNTable(data []*apiv2.VPNNode, _ bool) ([]string, [][]string, error) { + var ( + rows [][]string + ) + header := []string{"ID", "Name", "Project", "IPs", "Last Seed"} + + for _, node := range data { + lastSeen := node.LastSeen.AsTime().Format(time.DateTime + " MST") + ips := strings.Join(node.IpAddresses, ",") + + row := []string{ + fmt.Sprintf("%d", node.Id), + node.Name, + node.Project, + ips, + lastSeen, + } + + rows = append(rows, row) + } + + t.t.DisableAutoWrap(false) + + return header, rows, nil +} diff --git a/docs/metalctlv2.md b/docs/metalctlv2.md index 1187792..d17628f 100644 --- a/docs/metalctlv2.md +++ b/docs/metalctlv2.md @@ -27,8 +27,11 @@ 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 project](metalctlv2_project.md) - manage project entities +* [metalctlv2 size](metalctlv2_size.md) - manage size entities * [metalctlv2 tenant](metalctlv2_tenant.md) - manage tenant entities * [metalctlv2 token](metalctlv2_token.md) - manage token entities * [metalctlv2 user](metalctlv2_user.md) - manage user entities diff --git a/docs/metalctlv2_ip.md b/docs/metalctlv2_ip.md index afd5d6f..9b84e74 100644 --- a/docs/metalctlv2_ip.md +++ b/docs/metalctlv2_ip.md @@ -4,7 +4,7 @@ manage ip entities ### Synopsis -an ip address of metal-stack.io +manage ip addresses ### Options diff --git a/docs/metalctlv2_ip_describe.md b/docs/metalctlv2_ip_describe.md index b206813..9246979 100644 --- a/docs/metalctlv2_ip_describe.md +++ b/docs/metalctlv2_ip_describe.md @@ -9,8 +9,9 @@ metalctlv2 ip describe [flags] ### Options ``` - -h, --help help for describe - -p, --project string project of the ip + -h, --help help for describe + -n, --namespace string namespace of the ip + -p, --project string project of the ip ``` ### Options inherited from parent commands diff --git a/docs/metalctlv2_machine.md b/docs/metalctlv2_machine.md new file mode 100644 index 0000000..a1d79c0 --- /dev/null +++ b/docs/metalctlv2_machine.md @@ -0,0 +1,38 @@ +## metalctlv2 machine + +manage machine entities + +### Synopsis + +an machine of metal-stack.io + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..3412064 --- /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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..55a4022 --- /dev/null +++ b/docs/metalctlv2_machine_create.md @@ -0,0 +1,108 @@ +## metalctlv2 machine create + +creates the machine + +``` +metalctlv2 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 + +``` + -t, --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. + -d, --description string Description of the machine to create. [optional] + --dnsservers 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. + + --filesystemlayout string Filesystemlayout to use during machine installation. [optional] + -h, --help help for create + -H, --hostname string Hostname of the machine. [required] + -I, --id string ID of a specific machine to allocate, if given, size and partition are ignored. Need to be set to reserved (--reserve) state before. + -i, --image string OS Image to install. [required] + -n, --name string Name of the machine. [optional] + --networks strings Adds a network. Usage: [--networks NETWORK,[ip:ip:ip][,NETWORK...]... + 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. + + --ntpservers strings ntp servers to add to the machine or firewall. [optional] + -S, --partition string partition/datacenter where the machine is created. [required, except for reserved machines] + -P, --project string Project where the machine should belong to. [required] + -s, --size string Size of the machine. [required, except for reserved machines] + --skip-security-prompts skips security prompt for bulk operations + -p, --sshpublickey 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. + --tags strings tags to add to the machine, use it like: --tags "tag1,tag2" or --tags "tag3". + --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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..5fea632 --- /dev/null +++ b/docs/metalctlv2_machine_delete.md @@ -0,0 +1,46 @@ +## 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 + --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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..1871484 --- /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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..a42e4ac --- /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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..aedb9f0 --- /dev/null +++ b/docs/metalctlv2_machine_list.md @@ -0,0 +1,33 @@ +## metalctlv2 machine list + +list all machines + +``` +metalctlv2 machine list [flags] +``` + +### Options + +``` + -h, --help help for list + -p, --project string project from where machines 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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..1be3361 --- /dev/null +++ b/docs/metalctlv2_machine_update.md @@ -0,0 +1,46 @@ +## metalctlv2 machine update + +updates the machine + +``` +metalctlv2 machine 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. + -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 + --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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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_network.md b/docs/metalctlv2_network.md new file mode 100644 index 0000000..aa37230 --- /dev/null +++ b/docs/metalctlv2_network.md @@ -0,0 +1,39 @@ +## metalctlv2 network + +manage network entities + +### Synopsis + +networks can be attached to a machine or firewall such that they can communicate with each other. + +### Options + +``` + -h, --help help for network +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network apply](metalctlv2_network_apply.md) - applies one or more networks from a given file +* [metalctlv2 network create](metalctlv2_network_create.md) - creates the network +* [metalctlv2 network delete](metalctlv2_network_delete.md) - deletes the network +* [metalctlv2 network describe](metalctlv2_network_describe.md) - describes the network +* [metalctlv2 network edit](metalctlv2_network_edit.md) - edit the network through an editor and update +* [metalctlv2 network list](metalctlv2_network_list.md) - list all networks +* [metalctlv2 network list-base-networks](metalctlv2_network_list-base-networks.md) - lists base networks that can be used for network creation +* [metalctlv2 network update](metalctlv2_network_update.md) - updates the network + diff --git a/docs/metalctlv2_network_apply.md b/docs/metalctlv2_network_apply.md new file mode 100644 index 0000000..2686fb0 --- /dev/null +++ b/docs/metalctlv2_network_apply.md @@ -0,0 +1,46 @@ +## metalctlv2 network apply + +applies one or more networks from a given file + +``` +metalctlv2 network 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 network describe network-1 -o yaml > network.yaml + $ vi network.yaml + $ # either via stdin + $ cat network.yaml | metalctlv2 network apply -f - + $ # or via file + $ metalctlv2 network apply -f network.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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_create.md b/docs/metalctlv2_network_create.md new file mode 100644 index 0000000..2c21312 --- /dev/null +++ b/docs/metalctlv2_network_create.md @@ -0,0 +1,55 @@ +## metalctlv2 network create + +creates the network + +``` +metalctlv2 network create [flags] +``` + +### Options + +``` + --addressfamily string addressfamily of the network to acquire, if not specified the network inherits the address families from the parent [optional] + --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 network to create. [optional] + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 network describe network-1 -o yaml > network.yaml + $ vi network.yaml + $ # either via stdin + $ cat network.yaml | metalctlv2 network create -f - + $ # or via file + $ metalctlv2 network create -f network.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for create + --ipv4-prefix-length uint32 ipv4 prefix bit length of the network to create, defaults to default child prefix length of the parent network. [optional] + --ipv6-prefix-length uint32 ipv6 prefix bit length of the network to create, defaults to default child prefix length of the parent network. [optional] + --labels strings labels for this network. [optional] + --name string name of the network to create. [required] + --parent-network-id string the parent of the network (alternative to partition). [optional] + --partition string partition where this network should exist. [required] + --project string partition where this network should exist (alternative to parent-network-id). [optional] + --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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_delete.md b/docs/metalctlv2_network_delete.md new file mode 100644 index 0000000..294c63d --- /dev/null +++ b/docs/metalctlv2_network_delete.md @@ -0,0 +1,46 @@ +## metalctlv2 network delete + +deletes the network + +``` +metalctlv2 network 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 network describe network-1 -o yaml > network.yaml + $ vi network.yaml + $ # either via stdin + $ cat network.yaml | metalctlv2 network delete -f - + $ # or via file + $ metalctlv2 network delete -f network.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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_describe.md b/docs/metalctlv2_network_describe.md new file mode 100644 index 0000000..a1e49e1 --- /dev/null +++ b/docs/metalctlv2_network_describe.md @@ -0,0 +1,31 @@ +## metalctlv2 network describe + +describes the network + +``` +metalctlv2 network describe [flags] +``` + +### Options + +``` + -h, --help help for describe +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_edit.md b/docs/metalctlv2_network_edit.md new file mode 100644 index 0000000..486aa87 --- /dev/null +++ b/docs/metalctlv2_network_edit.md @@ -0,0 +1,31 @@ +## metalctlv2 network edit + +edit the network through an editor and update + +``` +metalctlv2 network 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_list-base-networks.md b/docs/metalctlv2_network_list-base-networks.md new file mode 100644 index 0000000..626be02 --- /dev/null +++ b/docs/metalctlv2_network_list-base-networks.md @@ -0,0 +1,42 @@ +## metalctlv2 network list-base-networks + +lists base networks that can be used for network creation + +``` +metalctlv2 network list-base-networks [flags] +``` + +### Options + +``` + --addressfamily string addressfamily to filter, either ipv4 or ipv6 [optional] + --description string description to filter [optional] + --destination-prefixes strings destination prefixes to filter + -h, --help help for list-base-networks + --id string ID to filter [optional] + --labels strings labels to filter [optional] + --name string name to filter [optional] + --partition string partition to filter [optional] + --prefixes strings prefixes to filter + --project string project to filter [optional] + -t, --type string type of the network. [optional] + --vrf uint32 vrf to filter [optional] +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_list.md b/docs/metalctlv2_network_list.md new file mode 100644 index 0000000..c96e935 --- /dev/null +++ b/docs/metalctlv2_network_list.md @@ -0,0 +1,44 @@ +## metalctlv2 network list + +list all networks + +``` +metalctlv2 network list [flags] +``` + +### Options + +``` + --addressfamily string addressfamily to filter, either ipv4 or ipv6 [optional] + --description string description to filter [optional] + --destination-prefixes strings destination prefixes to filter + -h, --help help for list + --id string ID to filter [optional] + --labels strings labels to filter [optional] + --name string name to filter [optional] + --parent-network-id string parent network to filter [optional] + --partition string partition to filter [optional] + --prefixes strings prefixes to filter + --project string project to filter [optional] + --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: description|id|name|partition|project + -t, --type string type of the network. [optional] + --vrf uint32 vrf to filter [optional] +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_network_update.md b/docs/metalctlv2_network_update.md new file mode 100644 index 0000000..1e8e5d0 --- /dev/null +++ b/docs/metalctlv2_network_update.md @@ -0,0 +1,50 @@ +## metalctlv2 network update + +updates the network + +``` +metalctlv2 network 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 the description of the network [optional] + -f, --file string filename of the create or update request in yaml format, or - for stdin. + + Example: + $ metalctlv2 network describe network-1 -o yaml > network.yaml + $ vi network.yaml + $ # either via stdin + $ cat network.yaml | metalctlv2 network update -f - + $ # or via file + $ metalctlv2 network update -f network.yaml + + the file can also contain multiple documents and perform a bulk operation. + + -h, --help help for update + --labels strings the labels of the network, must be in the form of key=value, use it like: --labels "key1=value1,key2=value2". [optional] + --name string the name of the network [optional] + --project string project to filter [optional] + --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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 network](metalctlv2_network.md) - manage network entities + diff --git a/docs/metalctlv2_size.md b/docs/metalctlv2_size.md new file mode 100644 index 0000000..02880f6 --- /dev/null +++ b/docs/metalctlv2_size.md @@ -0,0 +1,33 @@ +## metalctlv2 size + +manage size entities + +### Synopsis + +manage sizes which defines the cpu, gpu, memory and storage properties of machines + +### Options + +``` + -h, --help help for size +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 size describe](metalctlv2_size_describe.md) - describes the size +* [metalctlv2 size list](metalctlv2_size_list.md) - list all sizes + diff --git a/docs/metalctlv2_size_describe.md b/docs/metalctlv2_size_describe.md new file mode 100644 index 0000000..fe0978d --- /dev/null +++ b/docs/metalctlv2_size_describe.md @@ -0,0 +1,31 @@ +## metalctlv2 size describe + +describes the size + +``` +metalctlv2 size describe [flags] +``` + +### Options + +``` + -h, --help help for describe +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 size](metalctlv2_size.md) - manage size entities + diff --git a/docs/metalctlv2_size_list.md b/docs/metalctlv2_size_list.md new file mode 100644 index 0000000..0d13549 --- /dev/null +++ b/docs/metalctlv2_size_list.md @@ -0,0 +1,34 @@ +## metalctlv2 size list + +list all sizes + +``` +metalctlv2 size list [flags] +``` + +### Options + +``` + --description string size description to filter for + -h, --help help for list + --id string size id to filter for + --name string size name to filter for +``` + +### 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 (default "https://api.metal-stack.io") + -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|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used). (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 size](metalctlv2_size.md) - manage size entities + diff --git a/docs/metalctlv2_token.md b/docs/metalctlv2_token.md index 82cbe42..f4901f2 100644 --- a/docs/metalctlv2_token.md +++ b/docs/metalctlv2_token.md @@ -4,7 +4,7 @@ manage token entities ### Synopsis -manage api tokens for accessing the metal-stack.io api +manage api tokens ### Options diff --git a/go.mod b/go.mod index 294aa18..f4fcf27 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/metal-stack/cli -go 1.26 +go 1.26.0 require ( buf.build/go/protoyaml v0.7.0 @@ -19,7 +19,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.81.1 - google.golang.org/protobuf v1.36.11 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af sigs.k8s.io/yaml v1.6.0 ) @@ -29,15 +29,15 @@ require ( cel.dev/expr v0.25.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/displaywidth v0.10.0 // indirect - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-openapi/errors v0.22.7 // indirect - github.com/go-openapi/strfmt v0.26.1 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/go-openapi/strfmt v0.26.2 // 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/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/cel-go v0.28.1 // indirect @@ -45,20 +45,18 @@ require ( github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/connect-compress/v2 v2.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.21 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/minio/minlz v1.1.1 // indirect - github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.2.0 // indirect - github.com/olekukonko/ll v0.1.6 // 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.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect @@ -68,8 +66,7 @@ require ( golang.org/x/text v0.37.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apimachinery v0.35.1 // indirect + k8s.io/apimachinery v0.36.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect ) diff --git a/go.sum b/go.sum index 998864c..7a8c081 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,10 @@ github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo 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/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +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/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= @@ -31,22 +31,24 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= -github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= -github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= -github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= -github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/strfmt v0.26.2 h1:ysjheCh4i1rmFEo2LanhELDNucNzfWTZhUDKgWWPaFM= +github.com/go-openapi/strfmt v0.26.2/go.mod h1:fXh1e449cyUn2NYuz+wb3wARBUdMl7qPEZwX00nqivY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 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/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/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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -65,10 +67,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= -github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/metal-stack/api v0.1.0 h1:V3nrarxGexnE0f1/2UtGCIAtjP815p7+yqQvruD0KJQ= github.com/metal-stack/api v0.1.0/go.mod h1:wFU5aKfuurbBISQSIdJwBLQf4mTOBlERqWBzCafahEY= github.com/metal-stack/metal-lib v0.25.1 h1:z14xNl59ueQavNvMG4wcIZGqVyosNlaNFcy8hUu3SCU= @@ -81,15 +83,14 @@ github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= -github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= -github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U= +github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= +github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= @@ -121,32 +122,22 @@ 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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= -golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM= golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 h1:WVVw1Nl19li0fMX++FJ3ye1z9+S1N35QODDy5qpnaXw= -google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -154,8 +145,8 @@ 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= -k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= 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= diff --git a/pkg/common/ip.go b/pkg/common/ip.go new file mode 100644 index 0000000..4fea5cb --- /dev/null +++ b/pkg/common/ip.go @@ -0,0 +1,12 @@ +package common + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" +) + +func IpStaticToType(b bool) apiv2.IPType { + if b { + return apiv2.IPType_IP_TYPE_STATIC + } + return apiv2.IPType_IP_TYPE_EPHEMERAL +} diff --git a/pkg/common/network.go b/pkg/common/network.go new file mode 100644 index 0000000..dd6c1df --- /dev/null +++ b/pkg/common/network.go @@ -0,0 +1,33 @@ +package common + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" +) + +func IPAddressFamilyToType(af string) *apiv2.IPAddressFamily { + switch af { + case "": + return nil + case "ipv4", "IPv4": + return apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_V4.Enum() + case "ipv6", "IPv6": + return apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_V6.Enum() + default: + return apiv2.IPAddressFamily_IP_ADDRESS_FAMILY_UNSPECIFIED.Enum() + } +} + +func NetworkAddressFamilyToType(af string) *apiv2.NetworkAddressFamily { + switch af { + case "": + return nil + case "ipv4", "IPv4": + return apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V4.Enum() + case "ipv6", "IPv6": + return apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_V6.Enum() + case "dual-stack": + return apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_DUAL_STACK.Enum() + default: + return apiv2.NetworkAddressFamily_NETWORK_ADDRESS_FAMILY_UNSPECIFIED.Enum() + } +} 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/testing/e2e/test_framework.go b/testing/e2e/test_framework.go index ed574ff..bad8669 100644 --- a/testing/e2e/test_framework.go +++ b/testing/e2e/test_framework.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "reflect" "strings" "testing" "testing/synctest" @@ -13,10 +14,10 @@ import ( "slices" "buf.build/go/protoyaml" + "connectrpc.com/connect" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/metal-stack/metal-lib/pkg/pointer" - "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" @@ -77,7 +78,7 @@ func (c *Test[Response, RawObject]) TestCmd(t *testing.T) { synctest.Test(t, func(t *testing.T) { err := rootCmd.Execute() - if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { + if diff := cmp.Diff(c.WantErr, err, IgnoreUnexported(), ConnectErrorComparer()); diff != "" { t.Errorf("error diff (+got -want):\n %s", diff) } }) @@ -196,7 +197,7 @@ func (o *rawYamlOutputFormat[R]) Validate(t *testing.T, output []byte) { err := yaml.Unmarshal(output, &got) require.NoError(t, err) - if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(o.want, got, IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.Errorf("diff (+got -want):\n %s", diff) } } @@ -217,7 +218,7 @@ func (o *rawJsonOutputFormat[R]) Validate(t *testing.T, output []byte) { err := json.Unmarshal(output, &got) require.NoError(t, err) - if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(o.want, got, IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.Errorf("diff (+got -want):\n %s", diff) } } @@ -239,7 +240,7 @@ func (o *protoYAMLOutputFormat[R]) Validate(t *testing.T, output []byte) { err := protoyaml.Unmarshal(output, got) require.NoError(t, err) - if diff := cmp.Diff(o.want, got, protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(o.want, got, protocmp.Transform(), IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.Errorf("diff (+got -want):\n %s", diff) } } @@ -261,7 +262,7 @@ func (o *protoJSONOutputFormat[R]) Validate(t *testing.T, output []byte) { err := protojson.Unmarshal(output, got) require.NoError(t, err) - if diff := cmp.Diff(o.want, got, protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(o.want, got, protocmp.Transform(), IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.Errorf("diff (+got -want):\n %s", diff) } } @@ -392,3 +393,25 @@ func MustMarshalToMultiYAML(t *testing.T, data ...any) []byte { } return []byte(strings.Join(parts, "\n---\n")) } +func ConnectErrorComparer() cmp.Option { + return cmp.Comparer(func(x, y *connect.Error) bool { + if x == nil && y == nil { + return true + } + if x == nil && y != nil { + return false + } + if x != nil && y == nil { + return false + } + if x.Error() != y.Error() { + return false + } + return x.Code() == y.Code() + }) +} + +func IgnoreUnexported() cmp.Option { + // the exporter opt allows all unexported fields: https://github.com/google/go-cmp/pull/176 + return cmp.Exporter(func(reflect.Type) bool { return true }) +} diff --git a/tests/e2e/admin/component_test.go b/tests/e2e/admin/component_test.go index 28580ce..a15c66e 100644 --- a/tests/e2e/admin/component_test.go +++ b/tests/e2e/admin/component_test.go @@ -132,3 +132,139 @@ func Test_AdminComponentCmd_Delete(t *testing.T) { tt.TestCmd(t) } } + +func Test_AdminComponentCmd_List_Filter(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceListResponse, apiv2.Component]{ + { + Name: "list with uuid filter", + CmdArgs: []string{"admin", "component", "list", "--uuid", testresources.Component1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{ + Uuid: &testresources.Component1().Uuid, + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + testresources.Component1(), + }, + }) + }, + }, + }, + }), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` +c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 + `), + }, + { + Name: "list with type filter", + CmdArgs: []string{"admin", "component", "list", "--type", "metal-core"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{ + Type: &[]apiv2.ComponentType{apiv2.ComponentType_COMPONENT_TYPE_METAL_CORE}[0], + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + testresources.Component1(), + }, + }) + }, + }, + }, + }), + Template: new("{{ .uuid }} {{ .type }}"), + WantTemplate: new(` +c1a2b3d4-e5f6-7890-abcd-ef1234567890 2 + `), + }, + { + Name: "list with identifier filter", + CmdArgs: []string{"admin", "component", "list", "--identifier", testresources.Component1().Identifier}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{ + Identifier: &testresources.Component1().Identifier, + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + testresources.Component1(), + }, + }) + }, + }, + }, + }), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` +c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminComponentCmd_List_WithBothComponents(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceListResponse, apiv2.Component]{ + { + Name: "list both components", + CmdArgs: []string{"admin", "component", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + testresources.Component1(), + testresources.Component2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d + `), + WantWideTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d + `), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` +c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 +d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore-1 + `), + WantMarkdown: new(` + | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | + |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| + | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | + | d2b3c4e5-f6a7-8901-bcde-f12345678901 | pixiecore | pixiecore-1 | 0s | 0s | v2.0.0 | t2b3c4e5-f6a7-8901-bcde-f12345678901 | 2d | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/tests/e2e/admin/machine_test.go b/tests/e2e/admin/machine_test.go new file mode 100644 index 0000000..0f52c63 --- /dev/null +++ b/tests/e2e/admin/machine_test.go @@ -0,0 +1,171 @@ +package admin_e2e + +import ( + "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" + "github.com/metal-stack/cli/testing/e2e" +) + +var ( + machine1 = func() *apiv2.Machine { + return &apiv2.Machine{ + Uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "test": "value", + }, + }, + }, + Partition: &apiv2.Partition{ + Id: "test-partition", + }, + Rack: "rack-1", + Size: &apiv2.Size{Id: "test-size"}, + Hardware: &apiv2.MachineHardware{}, + Allocation: testMachineAllocation(), + Status: &apiv2.MachineStatus{ + Condition: &apiv2.MachineCondition{ + State: apiv2.MachineState_MACHINE_STATE_AVAILABLE, + Description: "available", + }, + Liveliness: apiv2.MachineLiveliness_MACHINE_LIVELINESS_ALIVE, + }, + RecentProvisioningEvents: &apiv2.MachineRecentProvisioningEvents{}, + } + } + machine2 = func() *apiv2.Machine { + return &apiv2.Machine{ + Uuid: "b2c3d4e5-f6a7-8901-bcde-f12345678901", + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "test": "another", + }, + }, + }, + Partition: &apiv2.Partition{ + Id: "test-partition", + }, + Rack: "rack-2", + Size: &apiv2.Size{Id: "test-size"}, + Hardware: &apiv2.MachineHardware{}, + Allocation: testMachineAllocation2(), + Status: &apiv2.MachineStatus{ + Condition: &apiv2.MachineCondition{ + State: apiv2.MachineState_MACHINE_STATE_AVAILABLE, + Description: "available", + }, + Liveliness: apiv2.MachineLiveliness_MACHINE_LIVELINESS_DEAD, + }, + RecentProvisioningEvents: &apiv2.MachineRecentProvisioningEvents{}, + } + } +) + +func testMachineAllocation() *apiv2.MachineAllocation { + return &apiv2.MachineAllocation{ + Uuid: "alloc-a1b2c3d4-e5f6-7890-abcd-ef1234567890", + Name: "machine-1", + Project: "test-project", + Hostname: "machine-1.test.local", + Image: &apiv2.Image{Name: new("ubuntu-24.04")}, + Meta: &apiv2.Meta{CreatedAt: nil}, + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + } +} + +func testMachineAllocation2() *apiv2.MachineAllocation { + return &apiv2.MachineAllocation{ + Uuid: "alloc-b2c3d4e5-f6a7-8901-bcde-f12345678901", + Name: "machine-2", + Project: "test-project", + Hostname: "machine-2.test.local", + Image: &apiv2.Image{Name: new("ubuntu-24.04")}, + Meta: &apiv2.Meta{CreatedAt: nil}, + AllocationType: apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE, + } +} + +func Test_AdminMachineCmd_Get(t *testing.T) { + tests := []*e2e.Test[adminv2.MachineServiceGetResponse, *apiv2.Machine]{ + { + Name: "get", + CmdArgs: []string{"admin", "machine", "get", machine1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.MachineServiceGetRequest{ + Uuid: machine1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.MachineServiceGetResponse{ + Machine: machine1(), + }) + }, + }, + }, + }), + WantObject: machine1(), + WantProtoObject: machine1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminMachineCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.MachineServiceListResponse, apiv2.Machine]{ + { + Name: "list", + CmdArgs: []string{"admin", "machine", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.MachineServiceListRequest{ + Query: &apiv2.MachineQuery{ + Labels: &apiv2.Labels{ + Labels: map[string]string{}, + }, + Allocation: &apiv2.MachineAllocationQuery{ + Labels: &apiv2.Labels{ + Labels: map[string]string{}, + }, + }, + Network: &apiv2.MachineNetworkQuery{}, + Nic: &apiv2.MachineNicQuery{}, + Disk: &apiv2.MachineDiskQuery{ + Names: []string{}, + }, + Bmc: &apiv2.MachineBMCQuery{}, + Fru: &apiv2.MachineFRUQuery{}, + Hardware: &apiv2.MachineHardwareQuery{}, + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.MachineServiceListResponse{ + Machines: []*apiv2.Machine{ + machine1(), + machine2(), + }, + }) + }, + }, + }, + }), + Template: new("{{ .uuid }} {{ .allocation.name }}"), + WantTemplate: new(` +a1b2c3d4-e5f6-7890-abcd-ef1234567890 machine-1 +b2c3d4e5-f6a7-8901-bcde-f12345678901 machine-2 + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/tests/e2e/api/ip_test.go b/tests/e2e/api/ip_test.go index 2500bca..433bc55 100644 --- a/tests/e2e/api/ip_test.go +++ b/tests/e2e/api/ip_test.go @@ -36,14 +36,14 @@ func Test_IPCmd_List(t *testing.T) { }, }), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), WantWideTable: new(` - IP PROJECT ID TYPE NAME DESCRIPTION LABELS - 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx + IP NS PROJECT ID TYPE NAME DESCRIPTION LABELS + 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx `), Template: new("{{ .ip }} {{ .project }}"), WantTemplate: new(` @@ -51,10 +51,10 @@ func Test_IPCmd_List(t *testing.T) { 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 `), WantMarkdown: new(` - | IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | - |---------|--------------------------------------|--------------------------------------|-----------|------|------------------| - | 4.3.2.1 | 46bdfc45-9c8d-4268-b359-b40e3079d384 | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | - | 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | + | IP | NS | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | + |---------|----|--------------------------------------|--------------------------------------|-----------|------|------------------| + | 4.3.2.1 | | 46bdfc45-9c8d-4268-b359-b40e3079d384 | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | + | 1.1.1.1 | | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | `), }, } @@ -86,21 +86,21 @@ func Test_IPCmd_Describe(t *testing.T) { WantObject: testresources.IP1(), WantProtoObject: testresources.IP1(), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), WantWideTable: new(` - IP PROJECT ID TYPE NAME DESCRIPTION LABELS - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx + IP NS PROJECT ID TYPE NAME DESCRIPTION LABELS + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx `), Template: new("{{ .ip }} {{ .project }}"), WantTemplate: new(` 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 `), WantMarkdown: new(` - | IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | - |---------|--------------------------------------|--------------------------------------|--------|------|------------------| - | 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | + | IP | NS | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | + |---------|----|--------------------------------------|--------------------------------------|--------|------|------------------| + | 1.1.1.1 | | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | `), }, } @@ -172,8 +172,8 @@ func Test_IPCmd_Create(t *testing.T) { }, }), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), }, } @@ -239,8 +239,8 @@ func Test_IPCmd_Delete(t *testing.T) { }, ), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), }, } @@ -277,13 +277,13 @@ func Test_IPCmd_Update(t *testing.T) { ), WantObject: testresources.IP1(), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), WantWideTable: new(` - IP PROJECT ID TYPE NAME DESCRIPTION LABELS - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx - `), + IP NS PROJECT ID TYPE NAME DESCRIPTION LABELS + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx + `), }, { Name: "update from file", @@ -329,8 +329,8 @@ func Test_IPCmd_Update(t *testing.T) { }, ), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), }, } @@ -382,8 +382,8 @@ func Test_IPCmd_Apply(t *testing.T) { }, ), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), }, { @@ -454,8 +454,8 @@ func Test_IPCmd_Apply(t *testing.T) { }, ), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP NS PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), }, }