diff --git a/go.sum b/go.sum index f387935f..c91a5420 100644 --- a/go.sum +++ b/go.sum @@ -153,7 +153,6 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ericlagergren/decimal v0.0.0-20190420051523-6335edbaa640/go.mod h1:mdYyfAkzn9kyJ/kMk/7WE9ufl9lflh+2NvecQ5mAghs= -github.com/ericlagergren/decimal v0.0.0-20211103172832-aca2edc11f73/go.mod h1:5sruVSMrZCk0U4hwRaGD0D8wIMFVsBWQqG74jQDFg4k= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -303,7 +302,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.5 h1:8IYp3w9nysqv3JH+NJgXJzGbDHzLOTj43BmSkp+O7qg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -667,12 +665,9 @@ github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5s github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY= github.com/volatiletech/sqlboiler v3.7.1+incompatible h1:dm9/NjDskQVwAarmpeZ2UqLn1NKE8M3WHSHBS4jw2x8= github.com/volatiletech/sqlboiler v3.7.1+incompatible/go.mod h1:jLfDkkHWPbS2cWRLkyC20vQWaIQsASEY7gM7zSo11Yw= -github.com/volatiletech/sqlboiler/v4 v4.14.2 h1:j5QnlR5/wYDmGDDTutI3BO+4oPBiqYoVrfReVr7VSxA= -github.com/volatiletech/sqlboiler/v4 v4.14.2/go.mod h1:65288sb8jBLnTynTumBK6eU8C2JwWsiPjoPihEfC0/A= github.com/volatiletech/sqlboiler/v4 v4.15.0 h1:+twm3mA34SaUF6wB9U6QkXxkK8AKkV5EfgMSvcKWeY4= github.com/volatiletech/sqlboiler/v4 v4.15.0/go.mod h1:s643wqYyCQ7Ak2hMVxH7kTS0+lFPNlj+gHKUIukJ0YA= github.com/volatiletech/strmangle v0.0.1/go.mod h1:F6RA6IkB5vq0yTG4GQ0UsbbRcl3ni9P76i+JrTBKFFg= -github.com/volatiletech/strmangle v0.0.4/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0= github.com/volatiletech/strmangle v0.0.5 h1:CompJPy+lAi9h+YU/IzBR4X2RDRuAuEIP+kjFdyZXcU= github.com/volatiletech/strmangle v0.0.5/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -690,8 +685,6 @@ go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+ go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.hollow.sh/toolbox v0.6.1 h1:3E6JofImSCe63XayczbGfDxIXUjmBziMBBmbwook8WA= go.hollow.sh/toolbox v0.6.1/go.mod h1:nl+5RDDyYY/+wukOUzHHX2mOyWKRjlTOXUcGxny+tns= -go.infratographer.com/x v0.3.6 h1:3wjfkKtjtZ3mmvzOvWka5HlHqsTR++RxfxZtP0YeJN8= -go.infratographer.com/x v0.3.6/go.mod h1:AMNcTkqb+yHLCbnZtiiHTC7QvN+4MOpzdOhqHXfKQUk= go.infratographer.com/x v0.3.7 h1:kkykoVtC8XrmvC4oZwHWa/15+dv9RhQHgSm8KoEb/Nc= go.infratographer.com/x v0.3.7/go.mod h1:/zbDM9njbWzUDCA9pkbi1z/v4VZjGsVHx+SPycSgIhg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -788,8 +781,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA= -golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -1080,7 +1071,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/dbtools/testtools.go b/internal/dbtools/testtools.go index 8a20bb0f..10a25f7a 100644 --- a/internal/dbtools/testtools.go +++ b/internal/dbtools/testtools.go @@ -116,6 +116,9 @@ func cleanDB(t *testing.T) { // don't delete the builtin ServerCredentialTypes. Those are expected to exist for the application to work deleteFixture(ctx, t, models.ServerCredentialTypes(models.ServerCredentialTypeWhere.Builtin.EQ(false))) + deleteFixture(ctx, t, models.AocMacAddresses()) + deleteFixture(ctx, t, models.BMCMacAddresses()) + deleteFixture(ctx, t, models.BomInfos()) testDB.Exec("SET sql_safe_updates = true;") } diff --git a/pkg/api/v1/bom.go b/pkg/api/v1/bom.go new file mode 100644 index 00000000..9fcb7b67 --- /dev/null +++ b/pkg/api/v1/bom.go @@ -0,0 +1,78 @@ +package serverservice + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/volatiletech/null/v8" + + "go.hollow.sh/serverservice/internal/models" +) + +// Bom provides a struct to map the bom_info table. +// Naming conversion is strange here just in order to make it consistent +// with generated BomInfo. +type Bom struct { + SerialNum string `json:"serial_num"` // physical serial number listed outside of a server + AocMacAddress string `json:"aoc_mac_address"` // Aoc is alternative name of the fiber channel card MAC address + BmcMacAddress string `json:"bmc_mac_address"` + NumDefiPmi string `json:"num_defi_pmi"` + NumDefPWD string `json:"num_def_pwd"` // DefPWD is the IPMI Password in the portal + Metro string `json:"metro"` +} + +// AocMacAddressBom provides a struct to map the aoc_mac_address table. +type AocMacAddressBom struct { + AocMacAddress string `json:"aoc_mac_address"` + SerialNum string `json:"serial_num"` +} + +// toDBModel converts Bom to BomInfo. +func (b *Bom) toDBModel() (*models.BomInfo, error) { + if b.SerialNum == "" { + return nil, errors.Errorf("the primary key serial-num can not be blank") + } + + dbB := &models.BomInfo{ + SerialNum: b.SerialNum, + AocMacAddress: null.StringFrom(b.AocMacAddress), + BMCMacAddress: null.StringFrom(b.BmcMacAddress), + NumDefiPmi: null.StringFrom(b.NumDefiPmi), + NumDefPWD: null.StringFrom(b.NumDefPWD), + Metro: null.StringFrom(b.Metro), + } + + return dbB, nil +} + +// toDBModel converts BomInfo to Bom. +func (b *Bom) fromDBModel(bomInfo *models.BomInfo) error { + b.SerialNum = bomInfo.SerialNum + b.AocMacAddress = bomInfo.AocMacAddress.String + b.BmcMacAddress = bomInfo.BMCMacAddress.String + b.NumDefiPmi = bomInfo.NumDefiPmi.String + b.NumDefPWD = bomInfo.NumDefPWD.String + b.Metro = bomInfo.Metro.String + + return nil +} + +// toAocMacAddressDBModels converts Bom to one or multiple AocMacAddress. +func (b *Bom) toAocMacAddressDBModels() ([]*models.AocMacAddress, error) { + if b.AocMacAddress == "" { + return nil, errors.Errorf("the primary key aoc-mac-address can not be blank") + } + + dbAs := []*models.AocMacAddress{} + + AocMacAddrs := strings.Split(b.AocMacAddress, ",") + for _, aocMacAddr := range AocMacAddrs { + dbA := &models.AocMacAddress{ + SerialNum: b.SerialNum, + AocMacAddress: aocMacAddr, + } + dbAs = append(dbAs, dbA) + } + + return dbAs, nil +} diff --git a/pkg/api/v1/router.go b/pkg/api/v1/router.go index 328a492a..b2f33792 100644 --- a/pkg/api/v1/router.go +++ b/pkg/api/v1/router.go @@ -121,6 +121,24 @@ func (r *Router) Routes(rg *gin.RouterGroup) { srvCmpntFwSets.DELETE("/:uuid", amw.RequiredScopes(deleteScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetDelete) srvCmpntFwSets.POST("/:uuid/remove-firmware", amw.RequiredScopes(deleteScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetRemoveFirmware) } + + // /bill-of-materials + srvBoms := rg.Group("/bill-of-materials") + { + // /bill-of-materials/batch-boms-upload + uploadFile := srvBoms.Group("/batch-upload") + { + uploadFile.POST("", amw.RequiredScopes(createScopes("batch-upload")), r.bomsUpload) + } + + // /bill-of-materials/aoc-mac-address + srvBomByAocMacAddress := srvBoms.Group("/aoc-mac-address") + { + srvBomByAocMacAddress.GET("/:aoc_mac_address", amw.RequiredScopes(readScopes("aoc-mac-address")), r.getBomFromAocMacAddress) + } + + // TODO: support query by bmc-mac-address + } } func createScopes(items ...string) []string { diff --git a/pkg/api/v1/router_bom.go b/pkg/api/v1/router_bom.go new file mode 100644 index 00000000..942b91c2 --- /dev/null +++ b/pkg/api/v1/router_bom.go @@ -0,0 +1,81 @@ +package serverservice + +import ( + "database/sql" + + "github.com/cockroachdb/cockroach-go/v2/crdb" + "github.com/gin-gonic/gin" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + + "go.hollow.sh/serverservice/internal/models" +) + +func (r *Router) bomsUpload(c *gin.Context) { + var boms []Bom + if err := c.ShouldBindJSON(&boms); err != nil { + badRequestResponse(c, "invalid payload: []Bom{}", err) + return + } + + err := crdb.ExecuteTx(c.Request.Context(), r.DB.DB, nil, func(tx *sql.Tx) error { + for _, bom := range boms { + dbBomInfo, err := (bom).toDBModel() + if err != nil { + return err + } + + if err := dbBomInfo.Insert(c.Request.Context(), r.DB, boil.Infer()); err != nil { + return err + } + + dbAocMacAddrsBoms, err := (bom).toAocMacAddressDBModels() + if err != nil { + return err + } + + for _, dbAocMacAddrsBom := range dbAocMacAddrsBoms { + if err := dbAocMacAddrsBom.Insert(c.Request.Context(), r.DB, boil.Infer()); err != nil { + return err + } + } + } + return nil + }) + if err != nil { + dbErrorResponse(c, err) + return + } + + createdResponse(c, "") +} + +func (r *Router) getBomFromAocMacAddress(c *gin.Context) { + mods := []qm.QueryMod{ + qm.Where("aoc_mac_address=?", c.Param("aoc_mac_address")), + } + + aocMacAddr, err := models.AocMacAddresses(mods...).One(c.Request.Context(), r.DB) + if err != nil { + dbErrorResponse(c, err) + return + } + + mods = []qm.QueryMod{ + qm.Where("serial_num=?", aocMacAddr.SerialNum), + } + + bomInfo, err := models.BomInfos(mods...).One(c.Request.Context(), r.DB) + if err != nil { + dbErrorResponse(c, err) + return + } + + bom := Bom{} + if err = bom.fromDBModel(bomInfo); err != nil { + dbErrorResponse(c, err) + return + } + + itemResponse(c, bom) +} diff --git a/pkg/api/v1/router_bom_test.go b/pkg/api/v1/router_bom_test.go new file mode 100644 index 00000000..f1c68b3b --- /dev/null +++ b/pkg/api/v1/router_bom_test.go @@ -0,0 +1,327 @@ +package serverservice_test + +import ( + "context" + "reflect" + "strings" + "testing" + + serverservice "go.hollow.sh/serverservice/pkg/api/v1" +) + +func TestIntegrationBomUpload(t *testing.T) { + testCases := []struct { + testName string + uploadBoms []serverservice.Bom + expectedUploadErrorMsg string + expectedUploadErr bool + aocMacAddress string + expectedAocMacAddressError bool + }{ + { + testName: "upload 1 bom and get by aoc mac address", + uploadBoms: []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + }, + expectedUploadErr: false, + expectedUploadErrorMsg: "", + aocMacAddress: "fakeAocMacAddress1", + expectedAocMacAddressError: false, + }, + { + testName: "upload 2 boms and get by aoc mac address", + uploadBoms: []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + { + SerialNum: "fakeSerialNum2", + AocMacAddress: "fakeAocMacAddress3,fakeAocMacAddress4", + BmcMacAddress: "fakeBmcMacAddress3,fakeBmcMacAddress4", + NumDefiPmi: "fakeNumDefipmi2", + NumDefPWD: "fakeNumDefpwd2", + Metro: "fakeMetro2", + }, + }, + expectedUploadErr: false, + expectedUploadErrorMsg: "", + aocMacAddress: "fakeAocMacAddress3", + expectedAocMacAddressError: false, + }, + { + testName: "upload duplicate serial number", + uploadBoms: []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress3,fakeAocMacAddress4", + BmcMacAddress: "fakeBmcMacAddress3,fakeBmcMacAddress4", + NumDefiPmi: "fakeNumDefipmi2", + NumDefPWD: "fakeNumDefpwd2", + Metro: "fakeMetro2", + }, + }, + expectedUploadErr: true, + expectedUploadErrorMsg: "unable to insert into bom_info: pq: duplicate key value violates unique constraint", + aocMacAddress: "fakeAocMacAddress3", + expectedAocMacAddressError: false, + }, + { + testName: "upload duplicate AocMacAddress", + uploadBoms: []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + { + SerialNum: "fakeSerialNum2", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress3", + BmcMacAddress: "fakeBmcMacAddress3,fakeBmcMacAddress4", + NumDefiPmi: "fakeNumDefipmi2", + NumDefPWD: "fakeNumDefpwd2", + Metro: "fakeMetro2", + }, + }, + expectedUploadErr: true, + expectedUploadErrorMsg: "unable to insert into aoc_mac_address: pq: duplicate key value violates unique constraint", + aocMacAddress: "fakeAocMacAddress3", + expectedAocMacAddressError: false, + }, + { + testName: "upload empty serial number", + uploadBoms: []serverservice.Bom{ + { + SerialNum: "", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + }, + expectedUploadErr: true, + expectedUploadErrorMsg: "the primary key serial-num can not be blank", + aocMacAddress: "fakeAocMacAddress3", + expectedAocMacAddressError: false, + }, + { + testName: "upload empty AocMacAddress", + uploadBoms: []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + }, + expectedUploadErr: true, + expectedUploadErrorMsg: "the primary key aoc-mac-address can not be blank", + aocMacAddress: "fakeAocMacAddress3", + expectedAocMacAddressError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + s := serverTest(t) + + authToken := validToken(adminScopes) + s.Client.SetToken(authToken) + + _, err := s.Client.BillOfMaterialsBatchUpload(context.TODO(), tc.uploadBoms) + if tc.expectedUploadErr { + if err == nil { + t.Fatalf("BillOfMaterialsBatchUpload(%v) expect error, got nil", tc.uploadBoms) + } + if !strings.Contains(err.Error(), tc.expectedUploadErrorMsg) { + t.Fatalf("BillOfMaterialsBatchUpload(%v) expect error %v, got %v", tc.uploadBoms, tc.expectedUploadErrorMsg, err) + } + return + } + if err != nil { + t.Fatalf("BillOfMaterialsBatchUpload(%v) failed to upload, err %v", tc.uploadBoms, err) + return + } + + _, _, err = s.Client.GetBomInfoByAOCMacAddr(context.TODO(), tc.aocMacAddress) + if tc.expectedAocMacAddressError { + if err == nil { + t.Fatalf("GetBomInfoByAOCMacAddr(%v) expect error, got nil", tc.aocMacAddress) + } + return + } + + if err != nil { + t.Fatalf("GetBomInfoByAOCMacAddr(%v) failed to get bom, err %v", tc.aocMacAddress, err) + } + }) + } +} + +func TestUploadInOneTransaction(t *testing.T) { + uploadBoms := + []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + { + SerialNum: "fakeSerialNum2", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress3", + BmcMacAddress: "fakeBmcMacAddress3,fakeBmcMacAddress4", + NumDefiPmi: "fakeNumDefipmi2", + NumDefPWD: "fakeNumDefpwd2", + Metro: "fakeMetro2", + }, + } + expectedUploadErrorMsg := "unable to insert into aoc_mac_address: pq: duplicate key value violates unique constraint" + expectedGetMsg := "no rows in result set" + + s := serverTest(t) + authToken := validToken(adminScopes) + s.Client.SetToken(authToken) + + _, err := s.Client.BillOfMaterialsBatchUpload(context.TODO(), uploadBoms) + if !strings.Contains(err.Error(), expectedUploadErrorMsg) { + t.Fatalf("BillOfMaterialsBatchUpload(%v) expect error %v, got %v", uploadBoms, expectedUploadErrorMsg, err) + } + + bom, _, err := s.Client.GetBomInfoByAOCMacAddr(context.TODO(), uploadBoms[0].AocMacAddress) + if err == nil || !strings.Contains(err.Error(), expectedGetMsg) { + t.Fatalf("GetBomInfoByAOCMacAddr(%v) got bom %v err %v, expect nil, %v", uploadBoms[0].AocMacAddress, bom, err, expectedGetMsg) + } +} + +func TestIntegrationGetBomByAocMacAddr(t *testing.T) { + s := serverTest(t) + + uploadBoms := []serverservice.Bom{ + { + SerialNum: "fakeSerialNum1", + AocMacAddress: "fakeAocMacAddress1,fakeAocMacAddress2", + BmcMacAddress: "fakeBmcMacAddress1,fakeBmcMacAddress2", + NumDefiPmi: "fakeNumDefipmi1", + NumDefPWD: "fakeNumDefpwd1", + Metro: "fakeMetro1", + }, + { + SerialNum: "fakeSerialNum2", + AocMacAddress: "fakeAocMacAddress3,fakeAocMacAddress4", + BmcMacAddress: "fakeBmcMacAddress3,fakeBmcMacAddress4", + NumDefiPmi: "fakeNumDefipmi2", + NumDefPWD: "fakeNumDefpwd2", + Metro: "fakeMetro2", + }, + } + authToken := validToken(adminScopes) + s.Client.SetToken(authToken) + + _, err := s.Client.BillOfMaterialsBatchUpload(context.TODO(), uploadBoms) + if err != nil { + t.Fatalf("s.Client.BillOfMaterialsBatchUpload(%v) failed to upload, err %v", uploadBoms, err) + return + } + + var testCases = []struct { + testName string + aocMacAddress string + expectedBom serverservice.Bom + expectedAocMacAddressError bool + expectedAocMacAddressErrorMsg string + }{ + { + testName: "get first bom by first aoc mac address", + aocMacAddress: "fakeAocMacAddress1", + expectedBom: uploadBoms[0], + expectedAocMacAddressError: false, + expectedAocMacAddressErrorMsg: "", + }, + { + testName: "get first bom by second aoc mac address", + aocMacAddress: "fakeAocMacAddress2", + expectedBom: uploadBoms[0], + expectedAocMacAddressError: false, + expectedAocMacAddressErrorMsg: "", + }, + { + testName: "get second bom by first aoc mac address", + aocMacAddress: "fakeAocMacAddress3", + expectedBom: uploadBoms[1], + expectedAocMacAddressError: false, + expectedAocMacAddressErrorMsg: "", + }, + { + testName: "get second bom by second aoc mac address", + aocMacAddress: "fakeAocMacAddress3", + expectedBom: uploadBoms[1], + expectedAocMacAddressError: false, + expectedAocMacAddressErrorMsg: "", + }, + { + testName: "non-exist aoc mac address", + aocMacAddress: "random", + expectedBom: uploadBoms[1], + expectedAocMacAddressError: true, + expectedAocMacAddressErrorMsg: "sql: no rows in result set", + }, + { + testName: "empty aoc mac address", + aocMacAddress: "", + expectedBom: uploadBoms[1], + expectedAocMacAddressError: true, + expectedAocMacAddressErrorMsg: "route not found", + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + bom, _, err := s.Client.GetBomInfoByAOCMacAddr(context.TODO(), tc.aocMacAddress) + if tc.expectedAocMacAddressError { + if err == nil { + t.Fatalf("GetBomInfoByAOCMacAddr(%v) expect error, got nil", tc.aocMacAddress) + } + if !strings.Contains(err.Error(), tc.expectedAocMacAddressErrorMsg) { + t.Fatalf("GetBomInfoByAOCMacAddr(%v) expect error %v, got %v", tc.aocMacAddress, tc.expectedAocMacAddressErrorMsg, err) + } + return + } + if err != nil { + t.Fatalf("GetBomInfoByAOCMacAddr(%v) failed to upload, err %v", tc.aocMacAddress, err) + return + } + + if !reflect.DeepEqual(bom, &tc.expectedBom) { + t.Fatalf("got incorrect bom %v, expect %v", bom, tc.expectedBom) + } + }) + } +} diff --git a/pkg/api/v1/server_service.go b/pkg/api/v1/server_service.go index 3cb26e3a..f2a1dc18 100644 --- a/pkg/api/v1/server_service.go +++ b/pkg/api/v1/server_service.go @@ -18,6 +18,9 @@ const ( serverCredentialsEndpoint = "credentials" serverCredentialTypeEndpoint = "server-credential-types" serverComponentFirmwareSetsEndpoint = "server-component-firmware-sets" + bomInfoEndpoint = "bill-of-materials" + uploadFileEndpoint = "batch-upload" + bomByMacAOCAddressEndpoint = "aoc-mac-address" ) // ClientInterface provides an interface for the expected calls to interact with a server service api @@ -54,6 +57,8 @@ type ClientInterface interface { SetCredential(context.Context, uuid.UUID, string, string) (*ServerResponse, error) DeleteCredential(context.Context, uuid.UUID, string) (*ServerResponse, error) ListServerCredentialTypes(context.Context) (*ServerResponse, error) + BillOfMaterialsBatchUpload(context.Context, []Bom) (*ServerResponse, error) + GetBomInfoByAOCMacAddr(context.Context, string) (*Bom, *ServerResponse, error) } // Create will attempt to create a server in Hollow and return the new server's UUID @@ -382,3 +387,28 @@ func (c *Client) ListServerCredentialTypes(ctx context.Context, params *Paginati func (c *Client) CreateServerCredentialType(ctx context.Context, sType *ServerCredentialType) (*ServerResponse, error) { return c.post(ctx, serverCredentialTypeEndpoint, sType) } + +// BillOfMaterialsBatchUpload will attempt to write multiple boms to database. +func (c *Client) BillOfMaterialsBatchUpload(ctx context.Context, boms []Bom) (*ServerResponse, error) { + path := fmt.Sprintf("%s/%s", bomInfoEndpoint, uploadFileEndpoint) + + resp, err := c.post(ctx, path, boms) + if err != nil { + return nil, err + } + + return resp, nil +} + +// GetBomInfoByAOCMacAddr will return the bom info object by the aoc mac address. +func (c *Client) GetBomInfoByAOCMacAddr(ctx context.Context, aocMacAddr string) (*Bom, *ServerResponse, error) { + path := fmt.Sprintf("%s/%s/%s", bomInfoEndpoint, bomByMacAOCAddressEndpoint, aocMacAddr) + bom := &Bom{} + r := ServerResponse{Record: bom} + + if err := c.get(ctx, path, &r); err != nil { + return nil, nil, err + } + + return bom, &r, nil +} diff --git a/pkg/api/v1/server_service_test.go b/pkg/api/v1/server_service_test.go index d396865c..117635ed 100644 --- a/pkg/api/v1/server_service_test.go +++ b/pkg/api/v1/server_service_test.go @@ -342,3 +342,42 @@ func TestServerServiceServerComponentFirmwareUpdate(t *testing.T) { return err }) } + +func TestBillOfMaterialsBatchUpload(t *testing.T) { + mockClientTests(t, func(ctx context.Context, respCode int, expectError bool) error { + bom := []hollow.Bom{{SerialNum: "fakeSerialNum1", AocMacAddress: "fakeAocMacAddress1", BmcMacAddress: "fakeBmcMacAddress1"}} + jsonResponse, err := json.Marshal(hollow.ServerResponse{Record: bom}) + require.Nil(t, err) + + c := mockClient(string(jsonResponse), respCode) + res, err := c.BillOfMaterialsBatchUpload(ctx, bom) + if !expectError { + assert.Equal(t, []interface{}([]interface{}{ + map[string]interface{}{ + "aoc_mac_address": "fakeAocMacAddress1", + "bmc_mac_address": "fakeBmcMacAddress1", + "metro": "", + "num_def_pwd": "", + "num_defi_pmi": "", + "serial_num": "fakeSerialNum1"}}), res.Record) + } + + return err + }) +} + +func TestGetBomInfoByAOCMacAddr(t *testing.T) { + mockClientTests(t, func(ctx context.Context, respCode int, expectError bool) error { + bom := hollow.Bom{SerialNum: "fakeSerialNum1", AocMacAddress: "fakeAocMacAddress1", BmcMacAddress: "fakeBmcMacAddress"} + jsonResponse, err := json.Marshal(hollow.ServerResponse{Record: bom}) + require.Nil(t, err) + + c := mockClient(string(jsonResponse), respCode) + respBom, _, err := c.GetBomInfoByAOCMacAddr(ctx, "fakeAocMacAddress1") + if !expectError { + assert.Equal(t, &bom, respBom) + } + + return err + }) +}