Skip to content

Commit

Permalink
add ngsm provider
Browse files Browse the repository at this point in the history
this is alpha code to:

- add the ngsm provider
- import HBOMs directly to netbox
- import from other sources (SLS, CMDB, hpcm.config, etc.)
- more to come...

example usage

```
export NETBOX_URL=https://127.0.0.1:8000
export NETBOX_TOKEN=
rm -rf ~/.cani
cani alpha session init ngsm --bom ./somehbom.csv
```

Signed-off-by: Jacob Salmela <[email protected]>
  • Loading branch information
jacobsalmela committed May 20, 2024
1 parent 3452be4 commit 4ac4759
Show file tree
Hide file tree
Showing 979 changed files with 587,785 additions and 4 deletions.
2 changes: 2 additions & 0 deletions cmd/session/session_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func initSessionWithProviderCmd(cmd *cobra.Command, args []string) (err error) {
switch root.D.Provider {
case taxonomy.CSM:
root.D.DatastorePath = filepath.Join(config.ConfigDir, taxonomy.DsFileCSM)
case taxonomy.Ngsm:
root.D.DatastorePath = filepath.Join(config.ConfigDir, taxonomy.DsFile)
default:
err = fmt.Errorf("not a valid provider: %s", root.D.Provider)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/taxonomy/taxonomy.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
const (
App = "cani"
CSM = "csm"
Ngsm = "ngsm"
DsFile = App + "db.json"
DsFileCSM = App + "db.json"
LogFile = App + "db.log"
Expand All @@ -49,7 +50,7 @@ const (
var (
DsPath = filepath.Join(CfgDir, DsFile)
CfgPath = filepath.Join(CfgDir, CfgFile)
SupportedProviders = []string{CSM}
SupportedProviders = []string{CSM, Ngsm}
)

func Init() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func MergeProviderFlags(providerCmd *cobra.Command, caniCmd *cobra.Command) (err
// at present, the user must run commands + provider name, like:
//
// cani add cabinet csm
// cani list blade hpengi
// cani list blade ngsm
//
// this requires hiding the provider sub command and dynamically executing it, as opposed to making the user type it in
func RegisterProviderCommand(p provider.InventoryProvider, caniCmd *cobra.Command) (err error) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/antihax/optional v1.0.0
github.com/fatih/color v1.13.0
github.com/fatih/structs v1.1.0
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
github.com/google/uuid v1.3.0
github.com/hashicorp/go-retryablehttp v0.7.2
github.com/manifoldco/promptui v0.9.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down
6 changes: 6 additions & 0 deletions internal/domain/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/Cray-HPE/cani/internal/inventory"
"github.com/Cray-HPE/cani/internal/provider"
"github.com/Cray-HPE/cani/internal/provider/csm"
"github.com/Cray-HPE/cani/internal/provider/ngsm"
"github.com/Cray-HPE/cani/pkg/hardwaretypes"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -90,6 +91,8 @@ func (d *Domain) SetupDomain(cmd *cobra.Command, args []string, configDomains ma
switch d.Provider {
case taxonomy.CSM:
d.datastore, err = inventory.NewDatastoreJSONCSM(d.DatastorePath, d.LogFilePath, inventory.Provider(d.Provider))
case taxonomy.Ngsm:
d.datastore, err = inventory.NewDatastoreJSON(d.DatastorePath, d.LogFilePath, inventory.Provider(d.Provider))
default:
return fmt.Errorf("invalid provider: %s", d.Provider)
}
Expand All @@ -116,6 +119,8 @@ func (d *Domain) SetupDomain(cmd *cobra.Command, args []string, configDomains ma
switch d.Provider {
case taxonomy.CSM:
d.externalInventoryProvider, err = csm.New(cmd, args, d.hardwareTypeLibrary, d.Options)
case taxonomy.Ngsm:
d.externalInventoryProvider, err = ngsm.New(cmd, args, d.hardwareTypeLibrary, d.Options)
default:
return fmt.Errorf("unknown external inventory provider provided (%s)", d.Provider)
}
Expand Down Expand Up @@ -166,6 +171,7 @@ type UpdatedHardwareResult struct {
func GetProviders() []provider.InventoryProvider {
supportedProviders := []provider.InventoryProvider{
&csm.CSM{},
&ngsm.Ngsm{},
}
return supportedProviders
}
Expand Down
3 changes: 3 additions & 0 deletions internal/domain/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"fmt"

"github.com/Cray-HPE/cani/internal/provider/csm"
"github.com/Cray-HPE/cani/internal/provider/ngsm"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
Expand All @@ -43,6 +44,8 @@ func NewProviderCmd(caniCmd *cobra.Command, p string) (providerCmd *cobra.Comman
switch p {
case "csm":
providerCmd, err = csm.NewProviderCmd(caniCmd)
case "ngsm":
providerCmd, err = ngsm.NewProviderCmd(caniCmd)
default:
err = fmt.Errorf("no command matched for provider %s", p)
}
Expand Down
39 changes: 39 additions & 0 deletions internal/provider/ngsm/build_hardware_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
*
* MIT License
*
* (C) Copyright 2023 Hewlett Packard Enterprise Development LP
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
package ngsm

import (
"github.com/Cray-HPE/cani/internal/inventory"
"github.com/Cray-HPE/cani/internal/provider"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

// BuildHardwareMetadata builds metadata, and adds it to the hardware object
func (ngsm *Ngsm) BuildHardwareMetadata(*inventory.Hardware, *cobra.Command, []string, provider.HardwareRecommendations) error {
log.Warn().Msgf("BuildHardwareMetadata not yet implemented")
return nil
}
183 changes: 183 additions & 0 deletions internal/provider/ngsm/expert_bom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
*
* MIT License
*
* (C) Copyright 2023 Hewlett Packard Enterprise Development LP
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
package ngsm

import (
"regexp"
"strconv"
"strings"

"github.com/rs/zerolog/log"
)

// Row is a structured row from the CSV
type Row struct {
ItemNumber string `csv:"Item#"`
Quantity int `csv:"Qty"`
ProductNumber string `csv:"Product #"`
ProductDescription string `csv:"Product Description"`
ConfigName string `csv:"Config Name"`
SolutionId int `csv:"Solution Id"`
SupportFor string `csv:"Support For"`
ClicStatus string `csv:"CLIC Status"`
UnitPrice float64 `csv:"Unit Price (USD) "`
ExtendedListPrice float64 `csv:"Extended List Price (USD)"`
ExpertBomComment string `csv:"Expert BOM Comment"`
Notes string `csv:"NOTES"`
netboxName string
source string
row int
rack string // when a device, the rack it will go in
netboxDeviceTypeID int32
}

// sanitize sanitizes an individual row in the bom
// At present, it only sanitizes the product number
// but other input validation could be added here
func (row *Row) sanitize() error {
row.ProductNumber = row.sanitizeProductNumber()
log.Trace().Msgf("Sanitized product number: %+v", row.ProductNumber)
return nil
}

// sanitizeProductNumber converts all blankspace in a product number to a
// single '-', making it easier for parsing and comparison
func (row *Row) sanitizeProductNumber() (productNumber string) {
// convert all whitespace to a single '-' character
re := regexp.MustCompile(`\s+`)
productNumber = re.ReplaceAllString(row.ProductNumber, "-")
return productNumber
}

// SetNetboxName sets the netbox name for the row
func (row *Row) SetNetboxName(name string) {
row.netboxName = name
}

// SetSource sets the source of the row, which is the usually the CSV filename
func (row *Row) SetSource(path string) {
row.source = path
}

// SetNetboxDeviceTypeID sets the netbox device type ID for the row
func (row *Row) SetNetboxDeviceTypeID(id int32) {
row.netboxDeviceTypeID = id
}

// SetRow sets the row number for the row
func (row *Row) SetRow(n int) {
row.row = n
}

// IsRack returns true if the row is a rack based on some fuzzy regex matching
func (row *Row) IsRack() bool {
// FIXME: do more fuzzy/regex checking
// or choose from pre-existing rack part numbers
// need multiple regexes because single there is no lookahead and lookbehind
// assertions in the regexp package
re1 := regexp.MustCompile(`\d+U`)
re2 := regexp.MustCompile(`[Rr]ack`)
re3 := regexp.MustCompile(`(?i)[Kk]it|[Ss]ervice`)
// if a row has both 'rack', a U count, and does not contain 'kit' or 'service', it is likely a rack
if re1.MatchString(row.ProductDescription) && re2.MatchString(row.ProductDescription) && !re3.MatchString(row.ProductDescription) {
return true
} else {
return false
}
}

// RackDimensions returns the width and depth of a rack in millimeters parsed
// via regex from the product description 'NNNmmxNNNmm'
func (row *Row) RackDimensions() (width int, depth int, unit string, err error) {
re := regexp.MustCompile(`\d+mmx\d+mm`)
dimensions := re.FindString(row.ProductDescription)
if dimensions != "" {
wh := strings.Split(dimensions, "x")
w := strings.TrimSuffix(wh[0], "mm")
d := strings.TrimSuffix(wh[1], "mm")
width, err = strconv.Atoi(w)
if err != nil {
return 0, 0, "", err
}
depth, err = strconv.Atoi(d)
if err != nil {
return 0, 0, "", err
}
}
return width, depth, "mm", nil
}

// RackDimensions returns the width and depth of a rack in millimeters parsed
// via regex from the product description 'NNNmmxNNNmm'
func (row *Row) RackHeightU() (u int, err error) {
re := regexp.MustCompile(`\d+U`)
height := re.FindString(row.ProductDescription)
if height != "" {
ustring := strings.TrimSuffix(height, "U")
u, err = strconv.Atoi(ustring)
if err != nil {
return 0, err
}
}
return u, nil
}

// IsDevice returns true if the row is a device based on the product number
// At present, this is the only method to determine if a row is a device since
// netbox requires the device type in order to add a device
// this could be expanded to include alternative methods to match other fields
// in the device type yaml format
func (row *Row) IsDevice(deviceTypeIds map[string]int32) bool {
_, ok := deviceTypeIds[row.ProductNumber]
if ok {
row.SetNetboxDeviceTypeID(deviceTypeIds[row.ProductNumber])
return true
}

return false
}

// NewRowFromRow returns a new row from the existing row
// this is often used to create a duplicate row if the quantity is more than 1
// a few fields need to be set by the consumer of this function, like the netbox
// namne, source, etc.
func (row *Row) NewRowFromRow() (newRow Row) {
newRow = Row{}
newRow.SetRow(row.row)
newRow.ItemNumber = row.ItemNumber
newRow.Quantity = 0
newRow.ProductNumber = row.ProductNumber
newRow.ProductDescription = row.ProductDescription
newRow.ConfigName = row.ConfigName
newRow.SolutionId = row.SolutionId
newRow.SupportFor = row.SupportFor
newRow.ClicStatus = row.ClicStatus
newRow.UnitPrice = row.UnitPrice
newRow.ExtendedListPrice = row.ExtendedListPrice
newRow.ExpertBomComment = row.ExpertBomComment
newRow.Notes = row.Notes
return newRow
}
Loading

0 comments on commit 4ac4759

Please sign in to comment.