Skip to content

Commit

Permalink
Merge pull request #172 from Cray-HPE/develop
Browse files Browse the repository at this point in the history
add multi-provider support
  • Loading branch information
jacobsalmela authored Dec 12, 2023
2 parents 5c8bd7b + ddda057 commit 7a4c222
Show file tree
Hide file tree
Showing 217 changed files with 62,575 additions and 2,423 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ generate-swagger-hsm-client: bin/swagger-codegen-cli.jar
go fmt ./pkg/hsm-client/...
goimports -w ./pkg/hsm-client

# Generate clients from the following swagger files:
# HPCM: ./pkg/hpcm-client/openapi.yaml
generate-swagger-hpcm-client: bin/swagger-codegen-cli.jar
java -jar bin/swagger-codegen-cli.jar generate -i ./pkg/hpcm-client/openapi.yml -l go -o ./pkg/hpcm-client/ -DpackageName=hpcm_client
go fmt ./pkg/hpcm-client/...
goimports -w ./pkg/hpcm-client

venv:
virtualenv -p python3 venv
source venv/bin/activate
Expand Down
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/3843505/235496554-806630e3-a818-4e04-8d46-6a024994d08f.png"" width="150" height="150" alt="cani">
<img src="docs/custom_theme/img/hpe_pri_wht_rev_rgb.png"" width="150" height="63" alt="HPE Logo">
<br>
<strong>cani: Cani's Automated Nomicon Inventory</strong>
<strong>Continuous And Never-ending Inventory</strong>
</p>

> Can I manage an inventory of an entire datacenter? From subfloor to top-of-rack, **yes** you can.
# Use Cases

# `cani` Converges Disparate Hardware Inventory Systems
- Migrating from one inventory system to another
- Validating exisiting inventory data
- Exporting to another inventory format
- Serving as a front-end for the UX, while the backend is replaced with something new
- Use as a new self-contained inventory system

You can inventory hardware with this utility. The tool itself generates a portable inventory format that can serve as the source of truth itself. It can also be used to transition from one inventory system to another, using `cani`'s portable format as an intermediate inventory.
# Add/Remove/Update Hardware Example

```shell
cani session init csm # start a session with a specific provider by importing data and converting it to CANI format
cani add cabinet hpe-ex4000 --auto --accept # add hardware (child hardware is added automatically: chassis, controllers, etc.)
cani add blade hpe-crayex-ex4252-compute-blade --auto --accept # add a blade to the cabinet (and any nodes that hardware contains)
cani update node --uuid abcdef12-3456-2789-abcd-ef1234567890 --role Management --subrole Worker --nid 4 # update hardware metadata
cani export --format hpcm # export to another inventory format (can also commit and init a new session with a different provider)
cani session apply --commit # apply changes, retaining the CANI format, and optionally posting data to the provider
```

## Portable Inventory Format

Expand All @@ -31,6 +44,7 @@ You can inventory hardware with this utility. The tool itself generates a porta
}
```

## External Inventory Providers
## Currently Supported Providers

`cani` has built-in support for CSM and can communicate with SLS and HSM. Support for other inventory providers is still under development.
- CSM
- HPCM (WIP)
6 changes: 0 additions & 6 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ import (
"github.com/spf13/cobra"
)

var (
vendor string
name string
u string
)

// AddCmd represents the switch add command
var AddCmd = &cobra.Command{
Use: "add",
Expand Down
41 changes: 11 additions & 30 deletions cmd/blade/add_blade.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ import (
"sort"

root "github.com/Cray-HPE/cani/cmd"
"github.com/Cray-HPE/cani/internal/domain"
"github.com/Cray-HPE/cani/internal/inventory"
"github.com/Cray-HPE/cani/internal/provider"
"github.com/Cray-HPE/cani/internal/tui"
"github.com/Cray-HPE/cani/pkg/hardwaretypes"
Expand All @@ -43,25 +41,17 @@ import (

// AddBladeCmd represents the blade add command
var AddBladeCmd = &cobra.Command{
Use: "blade",
Short: "Add blades to the inventory.",
Long: `Add blades to the inventory.`,
PersistentPreRunE: root.DatastoreExists, // A session must be active to write to a datastore
SilenceUsage: true, // Errors are more important than the usage
Args: validHardware, // Hardware can only be valid if defined in the hardware library
RunE: addBlade, // Add a blade when this sub-command is called
Use: "blade",
Short: "Add blades to the inventory.",
Long: `Add blades to the inventory.`,
PreRunE: validHardware, // Hardware can only be valid if defined in the hardware library
RunE: addBlade, // Add a blade when this sub-command is called
}

// addBlade adds a blade to the inventory
func addBlade(cmd *cobra.Command, args []string) error {
// Create a domain object to interact with the datastore
d, err := domain.New(root.Conf.Session.DomainOptions)
if err != nil {
return err
}

func addBlade(cmd *cobra.Command, args []string) (err error) {
if auto {
recommendations, err := d.Recommend(args[0])
recommendations, err := root.D.Recommend(cmd, args, auto)
if err != nil {
return err
}
Expand Down Expand Up @@ -95,7 +85,7 @@ func addBlade(cmd *cobra.Command, args []string) error {
}

// Add the blade from the inventory using domain methods
result, err := d.AddBlade(cmd.Context(), args[0], cabinet, chassis, blade)
result, err := root.D.AddBlade(cmd, args, cabinet, chassis, blade)
if errors.Is(err, provider.ErrDataValidationFailure) {
// TODO this validation error print logic could be shared

Expand Down Expand Up @@ -129,9 +119,6 @@ func addBlade(cmd *cobra.Command, args []string) error {
return err
}

// Use a map to track already added nodes.
newNodes := []domain.HardwareLocationPair{}

for _, result := range result.AddedHardware {
// If the type is a Node
if result.Hardware.Type == hardwaretypes.NodeBlade {
Expand All @@ -145,15 +132,9 @@ func addBlade(cmd *cobra.Command, args []string) error {
hardwaretypes.Node,
result.Hardware.ID.String(),
result.Location)
// Add the node to the map
newNodes = append(newNodes, result)
if root.Conf.Session.DomainOptions.Provider == string(inventory.CSMProvider) {
log.Info().Str("status", "SUCCESS").Msgf("%s was successfully staged to be added to the system", hardwaretypes.NodeBlade)
log.Info().Msgf("UUID: %s", result.Hardware.ID)
log.Info().Msgf("Cabinet: %d", cabinet)
log.Info().Msgf("Chassis: %d", chassis)
log.Info().Msgf("Blade: %d", blade)
}

log.Info().Str("status", "SUCCESS").Msgf("%s was successfully staged to be added to the system", hardwaretypes.NodeBlade)
root.D.PrintHardware(&result.Hardware)
}
}

Expand Down
9 changes: 1 addition & 8 deletions cmd/blade/list_blade.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import (
"text/tabwriter"

root "github.com/Cray-HPE/cani/cmd"
"github.com/Cray-HPE/cani/internal/domain"
"github.com/Cray-HPE/cani/internal/inventory"
"github.com/Cray-HPE/cani/pkg/hardwaretypes"
"github.com/google/uuid"
Expand All @@ -53,14 +52,8 @@ var ListBladeCmd = &cobra.Command{

// listBlade lists blades in the inventory
func listBlade(cmd *cobra.Command, args []string) error {
// Instantiate a new logic object to interact with the datastore
d, err := domain.New(root.Conf.Session.DomainOptions)
if err != nil {
return err
}

// Get the entire inventory
inv, err := d.List()
inv, err := root.D.List()
if err != nil {
return err
}
Expand Down
8 changes: 1 addition & 7 deletions cmd/blade/remove_blade.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
"fmt"

root "github.com/Cray-HPE/cani/cmd"
"github.com/Cray-HPE/cani/internal/domain"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
Expand All @@ -52,13 +51,8 @@ func removeBlade(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Need a UUID to remove: %s", err.Error())
}

d, err := domain.New(root.Conf.Session.DomainOptions)
if err != nil {
return err
}

// Remove the blade from the inventory
err = d.RemoveBlade(u, recursion)
err = root.D.RemoveBlade(u, recursion)
if err != nil {
return err
}
Expand Down
25 changes: 12 additions & 13 deletions cmd/blade/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,29 @@ import (
"os"

root "github.com/Cray-HPE/cani/cmd"
"github.com/Cray-HPE/cani/pkg/hardwaretypes"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

// validHardware checks that the hardware type is valid by comparing it against the list of hardware types
func validHardware(cmd *cobra.Command, args []string) error {
library, err := hardwaretypes.NewEmbeddedLibrary(root.Conf.Session.DomainOptions.CustomHardwareTypesDir)
if err != nil {
return err
}

// Get the list of hardware types that are blades
deviceTypes := library.GetDeviceTypesByHardwareType(hardwaretypes.NodeBlade)
func validHardware(cmd *cobra.Command, args []string) (err error) {
log.Debug().Msgf("Validating hardware %+v", root.D)
if cmd.Flags().Changed("list-supported-types") {
cmd.SetOut(os.Stdout)
for _, hw := range deviceTypes {
cmd.Printf("%s\n", hw.Slug)
for _, hw := range root.BladeTypes {
// print additional provider defaults
if root.Verbose {
cmd.Printf("%s\n", hw.Slug)
} else {
cmd.Printf("%s\n", hw.Slug)
}
}
os.Exit(0)
}

if len(args) == 0 {
bladeTypes := []string{}
for _, hw := range deviceTypes {
for _, hw := range root.BladeTypes {
bladeTypes = append(bladeTypes, hw.Slug)
}
return fmt.Errorf("No hardware type provided: Choose from: %s", bladeTypes)
Expand All @@ -63,7 +62,7 @@ func validHardware(cmd *cobra.Command, args []string) error {
// Check that each arg is a valid blade type
for _, arg := range args {
matchFound := false
for _, device := range deviceTypes {
for _, device := range root.BladeTypes {
if arg == device.Slug {
matchFound = true
break
Expand Down
70 changes: 18 additions & 52 deletions cmd/cabinet/add_cabinet.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ import (
"sort"

root "github.com/Cray-HPE/cani/cmd"
"github.com/Cray-HPE/cani/internal/domain"
"github.com/Cray-HPE/cani/internal/provider"
"github.com/Cray-HPE/cani/internal/provider/csm"
"github.com/Cray-HPE/cani/internal/tui"
"github.com/Cray-HPE/cani/pkg/hardwaretypes"
"github.com/rs/zerolog/log"
Expand All @@ -42,68 +40,45 @@ import (

// AddCabinetCmd represents the cabinet add command
var AddCabinetCmd = &cobra.Command{
Use: "cabinet",
Short: "Add cabinets to the inventory.",
Long: `Add cabinets to the inventory.`,
PersistentPreRunE: validFlagCombos, // Also ensures a session is active
Args: validHardware, // Hardware can only be valid if defined in the hardware library
SilenceUsage: true, // Errors are more important than the usage
RunE: addCabinet, // Add a cabinet when this sub-command is called
Use: "cabinet",
Short: "Add cabinets to the inventory.",
Long: `Add cabinets to the inventory.`,
PreRunE: validHardware, // Hardware can only be valid if defined in the hardware library
RunE: addCabinet, // Add a cabinet when this sub-command is called
}

// addCabinet adds a cabinet to the inventory
func addCabinet(cmd *cobra.Command, args []string) error {
// Create a domain object to interact with the datastore
d, err := domain.New(root.Conf.Session.DomainOptions)
func addCabinet(cmd *cobra.Command, args []string) (err error) {
var recommendations = provider.HardwareRecommendations{}
recommendations, err = root.D.Recommend(cmd, args, auto)
if err != nil {
return err
}

if auto {
recommendations, err := d.Recommend(args[0])
if err != nil {
return err
}
// get hardware recommendations from the provider
log.Info().Msgf("Querying inventory to suggest cabinet number and VLAN ID")
// set the vars to the recommendations
cabinetNumber = recommendations.CabinetOrdinal
vlanId = recommendations.ProviderMetadata[csm.ProviderMetadataVlanId].(int)
log.Debug().Msgf("Provider recommendations: %+v", recommendations)
log.Info().Msgf("Suggested cabinet number: %d", cabinetNumber)
log.Info().Msgf("Suggested VLAN ID: %d", vlanId)
if accept {
auto = true
} else {
// Prompt the user to confirm the suggestions
auto, err = tui.CustomConfirmation(
fmt.Sprintf("Would you like to accept the recommendations and add the %s", hardwaretypes.Cabinet))
// Prompt the user to confirm the suggestions
if !accept {
accept, err = tui.CustomConfirmation(fmt.Sprintf("Would you like to accept the recommendations and add the %s", hardwaretypes.Cabinet))
if err != nil {
return err
}
}

// If the user chose not to accept the suggestions, exit
if !auto {
if !accept {
log.Warn().Msgf("Aborted %s add", hardwaretypes.Cabinet)
fmt.Printf("\nAuto-generated values can be overridden by re-running the command with explicit values:\n")
fmt.Printf("\n\tcani alpha add %s %s --vlan-id %d --cabinet %d\n\n", cmd.Name(), args[0], vlanId, cabinetNumber)

return nil
}
}

// Push all the CLI flags that were provided into a generic map
// TODO Need to figure out how to specify to unset something
// Right now the build metadata function in the CSM provider will
// unset options if nil is passed in.
cabinetMetadata := map[string]interface{}{
csm.ProviderMetadataVlanId: vlanId,
// log the provider recommendations to the screen
recommendations.Print()
}

// Add the cabinet to the inventory using domain methods
result, err := d.AddCabinet(cmd.Context(), args[0], cabinetNumber, cabinetMetadata)
result, err := root.D.AddCabinet(cmd, args, recommendations)
if errors.Is(err, provider.ErrDataValidationFailure) {
// TODO the following should probably suggest commands to fix the issue?
log.Error().Msgf("Inventory data validation errors encountered")
for id, failedValidation := range result.ProviderValidationErrors {
log.Error().Msgf(" %s: %s", id, failedValidation.Hardware.LocationPath.String())
Expand All @@ -118,20 +93,11 @@ func addCabinet(cmd *cobra.Command, args []string) error {
return err
}

log.Info().Str("status", "SUCCESS").Msgf("%s %d was successfully staged to be added to the system", hardwaretypes.Cabinet, cabinetNumber)

// Use a map to track already added nodes.
newNodes := []domain.HardwareLocationPair{}

for _, result := range result.AddedHardware {
// If the type is a Node
if result.Hardware.Type == hardwaretypes.Cabinet {
log.Debug().Msgf("%s added at %s with parent %s (%s)", result.Hardware.Type, result.Location.String(), hardwaretypes.System, result.Hardware.Parent)
log.Info().Msgf("UUID: %s", result.Hardware.ID)
log.Info().Msgf("Cabinet Number: %d", cabinetNumber)
log.Info().Msgf("VLAN ID: %d", vlanId)
// Add the node to the map
newNodes = append(newNodes, result)
log.Info().Str("status", "SUCCESS").Msgf("%s %d was successfully staged to be added to the system", hardwaretypes.Cabinet, recommendations.CabinetOrdinal)
root.D.PrintHardware(&result.Hardware)
}
}

Expand Down
Loading

0 comments on commit 7a4c222

Please sign in to comment.