Skip to content

Commit

Permalink
Add possibility to list size reservations.
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit91 committed Jan 8, 2024
1 parent 91f4c59 commit 8c3ed2f
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 6 deletions.
121 changes: 120 additions & 1 deletion cmd/size.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (
"fmt"

"github.com/dustin/go-humanize"
"github.com/metal-stack/metal-go/api/client/machine"
"github.com/metal-stack/metal-go/api/client/partition"
"github.com/metal-stack/metal-go/api/client/project"
"github.com/metal-stack/metal-go/api/client/size"
"github.com/metal-stack/metal-go/api/models"
"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/metalctl/cmd/sorters"
"github.com/metal-stack/metalctl/cmd/tableprinters"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -71,7 +75,33 @@ func newSizeCmd(c *config) *cobra.Command {
tryCmd.Flags().StringP("memory", "M", "", "Memory of the hardware to try, can be given in bytes or any human readable size spec")
tryCmd.Flags().StringP("storagesize", "S", "", "Total storagesize of the hardware to try, can be given in bytes or any human readable size spec")

return genericcli.NewCmds(cmdsConfig, newSizeImageConstraintCmd(c), tryCmd)
reservationsCmd := &cobra.Command{
Use: "reservations",
Short: "manage size reservations",
RunE: func(cmd *cobra.Command, args []string) error {
return w.listReverations()
},
}

listReservationsCmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "list size reservations",
RunE: func(cmd *cobra.Command, args []string) error {
return w.listReverations()
},
}
checkReservationsCmd := &cobra.Command{
Use: "check",
Short: "check if there are size reservations that are ineffective, e.g. because a project was deleted",
RunE: func(cmd *cobra.Command, args []string) error {
return w.checkReverations()
},
}

reservationsCmd.AddCommand(listReservationsCmd, checkReservationsCmd)

return genericcli.NewCmds(cmdsConfig, newSizeImageConstraintCmd(c), tryCmd, reservationsCmd)
}

func (c sizeCmd) Get(id string) (*models.V1SizeResponse, error) {
Expand Down Expand Up @@ -204,3 +234,92 @@ func (c *sizeCmd) try() error {

return c.listPrinter.Print(resp.Payload)
}

func (c sizeCmd) listReverations() error {
sizes, err := c.client.Size().ListSizes(size.NewListSizesParams(), nil)
if err != nil {
return err
}

projects, err := c.client.Project().ListProjects(project.NewListProjectsParams(), nil)
if err != nil {
return err
}

machines, err := c.client.Machine().ListMachines(machine.NewListMachinesParams(), nil)
if err != nil {
return err
}

return c.listPrinter.Print(&tableprinters.SizeReservations{
Sizes: sizes.Payload,
Projects: projects.Payload,
Machines: machines.Payload,
})
}

func (c sizeCmd) checkReverations() error {
sizes, err := c.client.Size().ListSizes(size.NewListSizesParams(), nil)
if err != nil {
return err
}

projects, err := c.client.Project().ListProjects(project.NewListProjectsParams(), nil)
if err != nil {
return err
}

partitions, err := c.client.Partition().ListPartitions(partition.NewListPartitionsParams(), nil)
if err != nil {
return err
}

var (
errs []error

projectsByID = tableprinters.ProjectsByID(projects.Payload)
partitionsByID = map[string]*models.V1PartitionResponse{}
)

for _, p := range partitions.Payload {
p := p
partitionsByID[*p.ID] = p
}

for _, size := range sizes.Payload {
size := size

for _, reservation := range size.Reservations {
reservation := reservation

var (
sizeID = pointer.SafeDeref(size.ID)
projectID = pointer.SafeDeref(reservation.Projectid)
)

for _, partition := range reservation.Partitionids {
_, ok := partitionsByID[partition]
if !ok {
errs = append(errs, fmt.Errorf("size reservation for size %q and project %q references a non-existing partition %q", sizeID, projectID, partition))
}
}

_, ok := projectsByID[projectID]
if !ok {
errs = append(errs, fmt.Errorf("size reservation for size %q references a non-existing project %q", sizeID, projectID))
}
}
}

if len(errs) == 0 {
fmt.Fprintln(c.out, "all size reservations are effective")
return nil
} else {
for _, err := range errs {
fmt.Fprintln(c.out, "found ineffective size reservations:")
fmt.Fprintln(c.out, err.Error())
}
}

return nil
}
14 changes: 10 additions & 4 deletions cmd/size_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ var (
Projectid: pointer.Pointer(project2.Meta.ID),
},
},
Labels: map[string]string{
"size.metal-stack.io/cpu-description": "1x Intel(R) Xeon(R) D-2141I CPU @ 2.20GHz",
"size.metal-stack.io/drive-description": "960GB NVMe",
},
Description: "size 1",
ID: pointer.Pointer("1"),
Name: "size-1",
Expand Down Expand Up @@ -103,8 +107,9 @@ ID NAME DESCRIPTION RESERVATIONS CPU RANGE MEMORY RANGE STORAGE RA
2 size-2 size 2 0 5 - 6 3 B - 4 B 1 B - 2 B
`),
wantWideTable: pointer.Pointer(`
ID NAME DESCRIPTION RESERVATIONS CPU RANGE MEMORY RANGE STORAGE RANGE
1 size-1 size 1 7 5 - 6 3 B - 4 B 1 B - 2 B
ID NAME DESCRIPTION RESERVATIONS CPU RANGE MEMORY RANGE STORAGE RANGE LABELS
1 size-1 size 1 7 5 - 6 3 B - 4 B 1 B - 2 B size.metal-stack.io/cpu-description=1x Intel(R) Xeon(R) D-2141I CPU @ 2.20GHz
size.metal-stack.io/drive-description=960GB NVMe
2 size-2 size 2 0 5 - 6 3 B - 4 B 1 B - 2 B
`),
template: pointer.Pointer("{{ .id }} {{ .name }}"),
Expand Down Expand Up @@ -226,8 +231,9 @@ ID NAME DESCRIPTION RESERVATIONS CPU RANGE MEMORY RANGE STORAGE RA
1 size-1 size 1 7 5 - 6 3 B - 4 B 1 B - 2 B
`),
wantWideTable: pointer.Pointer(`
ID NAME DESCRIPTION RESERVATIONS CPU RANGE MEMORY RANGE STORAGE RANGE
1 size-1 size 1 7 5 - 6 3 B - 4 B 1 B - 2 B
ID NAME DESCRIPTION RESERVATIONS CPU RANGE MEMORY RANGE STORAGE RANGE LABELS
1 size-1 size 1 7 5 - 6 3 B - 4 B 1 B - 2 B size.metal-stack.io/cpu-description=1x Intel(R) Xeon(R) D-2141I CPU @ 2.20GHz
size.metal-stack.io/drive-description=960GB NVMe
`),
template: pointer.Pointer("{{ .id }} {{ .name }}"),
wantTemplate: pointer.Pointer(`
Expand Down
2 changes: 2 additions & 0 deletions cmd/tableprinters/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin
return t.SizeMatchingLogTable(pointer.WrapInSlice(d), wide)
case []*models.V1SizeMatchingLog:
return t.SizeMatchingLogTable(d, wide)
case *SizeReservations:
return t.SizeReservationTable(d, wide)
default:
return nil, nil, fmt.Errorf("unknown table printer for type: %T", d)
}
Expand Down
97 changes: 96 additions & 1 deletion cmd/tableprinters/size.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ package tableprinters

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/dustin/go-humanize"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/genericcli"
"github.com/metal-stack/metal-lib/pkg/pointer"
"github.com/olekukonko/tablewriter"
)

type SizeReservations struct {
Projects []*models.V1ProjectResponse
Sizes []*models.V1SizeResponse
Machines []*models.V1MachineResponse
}

func (t *TablePrinter) SizeTable(data []*models.V1SizeResponse, wide bool) ([]string, [][]string, error) {
var (
header = []string{"ID", "Name", "Description", "Reservations", "CPU Range", "Memory Range", "Storage Range"}
rows [][]string
)

if wide {
header = []string{"ID", "Name", "Description", "Reservations", "CPU Range", "Memory Range", "Storage Range", "Labels"}
}

for _, size := range data {
var cpu, memory, storage string
for _, c := range size.Constraints {
Expand All @@ -35,9 +48,21 @@ func (t *TablePrinter) SizeTable(data []*models.V1SizeResponse, wide bool) ([]st
reservationCount += int(pointer.SafeDeref(r.Amount))
}

rows = append(rows, []string{pointer.SafeDeref(size.ID), size.Name, size.Description, strconv.Itoa(reservationCount), cpu, memory, storage})
row := []string{pointer.SafeDeref(size.ID), size.Name, size.Description, strconv.Itoa(reservationCount), cpu, memory, storage}

if wide {
labels := genericcli.MapToLabels(size.Labels)
sort.Strings(labels)
row = append(row, strings.Join(labels, "\n"))
}

rows = append(rows, row)
}

t.t.MutateTable(func(table *tablewriter.Table) {
table.SetAutoWrapText(false)
})

return header, rows, nil
}

Expand Down Expand Up @@ -72,3 +97,73 @@ func (t *TablePrinter) SizeMatchingLogTable(data []*models.V1SizeMatchingLog, wi

return header, rows, nil
}

func (t *TablePrinter) SizeReservationTable(data *SizeReservations, wide bool) ([]string, [][]string, error) {
var (
header = []string{"Partition", "Tenant", "Project", "Project Name", "Used/Amount", "Total Allocations"}
rows [][]string
)

projectsByID := ProjectsByID(data.Projects)
machinesByProject := map[string][]*models.V1MachineResponse{}
for _, m := range data.Machines {
m := m
if m.Allocation == nil || m.Allocation.Project == nil {
continue
}

machinesByProject[*m.Allocation.Project] = append(machinesByProject[*m.Allocation.Project], m)
}

for _, d := range data.Sizes {
d := d
for _, reservation := range d.Reservations {
if reservation.Projectid == nil {
continue
}

for _, partitionID := range reservation.Partitionids {
var (
projectName string
tenant string
)

project, ok := projectsByID[*reservation.Projectid]
if ok {
projectName = project.Name
tenant = project.TenantID
}

projectMachineCount := len(machinesByProject[*reservation.Projectid])
maxReservationCount := int(pointer.SafeDeref(reservation.Amount))

rows = append(rows, []string{
partitionID,
projectName,
*reservation.Projectid,
tenant,
fmt.Sprintf("%d/%d", min(maxReservationCount, projectMachineCount), maxReservationCount),
strconv.Itoa(projectMachineCount),
})
}
}
}

return header, rows, nil
}

func ProjectsByID(projects []*models.V1ProjectResponse) map[string]*models.V1ProjectResponse {
projectsByID := map[string]*models.V1ProjectResponse{}

for _, project := range projects {
project := project

if project.Meta == nil {
continue
}

projectsByID[project.Meta.ID] = project
}

return projectsByID
}
1 change: 1 addition & 0 deletions docs/metalctl_size.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ a size matches a machine in terms of cpu cores, ram and storage.
* [metalctl size edit](metalctl_size_edit.md) - edit the size through an editor and update
* [metalctl size imageconstraint](metalctl_size_imageconstraint.md) - manage imageconstraint entities
* [metalctl size list](metalctl_size_list.md) - list all sizes
* [metalctl size reservations](metalctl_size_reservations.md) - manage size reservations
* [metalctl size try](metalctl_size_try.md) - try a specific hardware spec and give the chosen size back
* [metalctl size update](metalctl_size_update.md) - updates the size

48 changes: 48 additions & 0 deletions docs/metalctl_size_reservations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## metalctl size reservations

manage size reservations

```
metalctl size reservations [flags]
```

### Options

```
-h, --help help for reservations
```

### Options inherited from parent commands

```
--api-token string api token to authenticate. Can be specified with METALCTL_API_TOKEN environment variable.
--api-url string api server address. Can be specified with METALCTL_API_URL environment variable.
-c, --config string alternative config file path, (default is ~/.metalctl/config.yaml).
Example config.yaml:
---
apitoken: "alongtoken"
...
--debug debug output
--force-color force colored output even without tty
--kubeconfig string Path to the kube-config to use for authentication and authorization. Is updated by login. Uses default path if not specified.
--no-headers do not print headers of table output format (default print headers)
-o, --output-format string output format (table|wide|markdown|json|yaml|template), wide is a table with more columns. (default "table")
--template string output template for template output-format, go template format.
For property names inspect the output of -o json or -o yaml for reference.
Example for machines:
metalctl machine list -o template --template "{{ .id }}:{{ .size.id }}"
--yes-i-really-mean-it skips security prompts (which can be dangerous to set blindly because actions can lead to data loss or additional costs)
```

### SEE ALSO

* [metalctl size](metalctl_size.md) - manage size entities
* [metalctl size reservations check](metalctl_size_reservations_check.md) - check if there are size reservations that are ineffective, e.g. because a project was deleted
* [metalctl size reservations list](metalctl_size_reservations_list.md) - list size reservations

Loading

0 comments on commit 8c3ed2f

Please sign in to comment.