From ce5bfcfdc475d1b950a0e961ce4ae1273616758c Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Mon, 6 Jan 2025 21:26:51 -0500 Subject: [PATCH] add redundancy group info methods --- apstra/client.go | 1 + apstra/enum/enums.go | 25 ++++ apstra/enum/generated_enums.go | 115 +++++++++++++++++ apstra/helpers_test.go | 27 ++-- ...two_stage_l3_clos_redundancy_group_info.go | 118 ++++++++++++++++++ ..._redundancy_group_info_integration_test.go | 76 +++++++++++ 6 files changed, 344 insertions(+), 18 deletions(-) create mode 100644 apstra/two_stage_l3_clos_redundancy_group_info.go create mode 100644 apstra/two_stage_l3_clos_redundancy_group_info_integration_test.go diff --git a/apstra/client.go b/apstra/client.go index f305aa35..01c65602 100644 --- a/apstra/client.go +++ b/apstra/client.go @@ -30,6 +30,7 @@ const ( ErrCannotChangeTransform ErrRangeOverlap ErrAuthFail + ErrInvalidApiResponse ErrCompatibility ErrConflict ErrExists diff --git a/apstra/enum/enums.go b/apstra/enum/enums.go index 4b66425c..7765f1b5 100644 --- a/apstra/enum/enums.go +++ b/apstra/enum/enums.go @@ -227,3 +227,28 @@ var ( VnTypeVlan = VnType{Value: "vlan"} VnTypeVxlan = VnType{Value: "vxlan"} ) + +type RgType oenum.Member[string] + +var ( + RgTypeEsi = RgType{Value: "esi"} + RgTypeMlag = RgType{Value: "mlag"} +) + +type SystemType oenum.Member[string] + +var ( + SystemTypeServer = SystemType{Value: "server"} + SystemTypeSwitch = SystemType{Value: "switch"} +) + +type NodeRole oenum.Member[string] + +var ( + NodeRoleAccess = NodeRole{Value: "access"} + NodeRoleGeneric = NodeRole{Value: "generic"} + NodeRoleLeaf = NodeRole{Value: "leaf"} + NodeRoleRemoteGateway = NodeRole{Value: "remote_gateway"} + NodeRoleSpine = NodeRole{Value: "spine"} + NodeRoleSuperspine = NodeRole{Value: "superspine"} +) diff --git a/apstra/enum/generated_enums.go b/apstra/enum/generated_enums.go index 96a32318..4075dfc0 100644 --- a/apstra/enum/generated_enums.go +++ b/apstra/enum/generated_enums.go @@ -322,6 +322,37 @@ func (o *JunosEvpnIrbMode) UnmarshalJSON(bytes []byte) error { return o.FromString(s) } +var ( + _ enum = (*NodeRole)(nil) + _ json.Marshaler = (*NodeRole)(nil) + _ json.Unmarshaler = (*NodeRole)(nil) +) + +func (o NodeRole) String() string { + return o.Value +} + +func (o *NodeRole) FromString(s string) error { + if NodeRoles.Parse(s) == nil { + return newEnumParseError(o, s) + } + o.Value = s + return nil +} + +func (o *NodeRole) MarshalJSON() ([]byte, error) { + return json.Marshal(o.String()) +} + +func (o *NodeRole) UnmarshalJSON(bytes []byte) error { + var s string + err := json.Unmarshal(bytes, &s) + if err != nil { + return err + } + return o.FromString(s) +} + var ( _ enum = (*PolicyApplicationPointType)(nil) _ json.Marshaler = (*PolicyApplicationPointType)(nil) @@ -539,6 +570,37 @@ func (o *ResourcePoolType) UnmarshalJSON(bytes []byte) error { return o.FromString(s) } +var ( + _ enum = (*RgType)(nil) + _ json.Marshaler = (*RgType)(nil) + _ json.Unmarshaler = (*RgType)(nil) +) + +func (o RgType) String() string { + return o.Value +} + +func (o *RgType) FromString(s string) error { + if RgTypes.Parse(s) == nil { + return newEnumParseError(o, s) + } + o.Value = s + return nil +} + +func (o *RgType) MarshalJSON() ([]byte, error) { + return json.Marshal(o.String()) +} + +func (o *RgType) UnmarshalJSON(bytes []byte) error { + var s string + err := json.Unmarshal(bytes, &s) + if err != nil { + return err + } + return o.FromString(s) +} + var ( _ enum = (*RoutingZoneConstraintMode)(nil) _ json.Marshaler = (*RoutingZoneConstraintMode)(nil) @@ -663,6 +725,37 @@ func (o *SviIpv6Mode) UnmarshalJSON(bytes []byte) error { return o.FromString(s) } +var ( + _ enum = (*SystemType)(nil) + _ json.Marshaler = (*SystemType)(nil) + _ json.Unmarshaler = (*SystemType)(nil) +) + +func (o SystemType) String() string { + return o.Value +} + +func (o *SystemType) FromString(s string) error { + if SystemTypes.Parse(s) == nil { + return newEnumParseError(o, s) + } + o.Value = s + return nil +} + +func (o *SystemType) MarshalJSON() ([]byte, error) { + return json.Marshal(o.String()) +} + +func (o *SystemType) UnmarshalJSON(bytes []byte) error { + var s string + err := json.Unmarshal(bytes, &s) + if err != nil { + return err + } + return o.FromString(s) +} + var ( _ enum = (*TcpStateQualifier)(nil) _ json.Marshaler = (*TcpStateQualifier)(nil) @@ -800,6 +893,16 @@ var ( JunosEvpnIrbModeAsymmetric, ) + _ enum = new(NodeRole) + NodeRoles = oenum.New( + NodeRoleAccess, + NodeRoleGeneric, + NodeRoleLeaf, + NodeRoleRemoteGateway, + NodeRoleSpine, + NodeRoleSuperspine, + ) + _ enum = new(PolicyApplicationPointType) PolicyApplicationPointTypes = oenum.New( PolicyApplicationPointTypeGroup, @@ -859,6 +962,12 @@ var ( ResourcePoolTypeVni, ) + _ enum = new(RgType) + RgTypes = oenum.New( + RgTypeEsi, + RgTypeMlag, + ) + _ enum = new(RoutingZoneConstraintMode) RoutingZoneConstraintModes = oenum.New( RoutingZoneConstraintModeNone, @@ -906,6 +1015,12 @@ var ( SviIpv6ModeLinkLocal, ) + _ enum = new(SystemType) + SystemTypes = oenum.New( + SystemTypeServer, + SystemTypeSwitch, + ) + _ enum = new(TcpStateQualifier) TcpStateQualifiers = oenum.New( TcpStateQualifierEstablished, diff --git a/apstra/helpers_test.go b/apstra/helpers_test.go index 6b710415..3c3d855a 100644 --- a/apstra/helpers_test.go +++ b/apstra/helpers_test.go @@ -360,7 +360,7 @@ func testBlueprintD(ctx context.Context, t *testing.T, client *Client) *TwoStage return bpClient } -func testBlueprintE(ctx context.Context, t *testing.T, client *Client) (*TwoStageL3ClosClient, func(context.Context) error) { +func testBlueprintE(ctx context.Context, t *testing.T, client *Client) *TwoStageL3ClosClient { bpId, err := client.CreateBlueprintFromTemplate(ctx, &CreateBlueprintFromTemplateRequest{ RefDesign: RefDesignTwoStageL3Clos, Label: randString(5, "hex"), @@ -374,10 +374,7 @@ func testBlueprintE(ctx context.Context, t *testing.T, client *Client) (*TwoStag if err != nil { t.Fatal(err) } - - bpDeleteFunc := func(ctx context.Context) error { - return client.DeleteBlueprint(ctx, bpId) - } + t.Cleanup(func() { require.NoError(t, client.DeleteBlueprint(ctx, bpId)) }) leafQuery := new(PathQuery). SetBlueprintId(bpId). @@ -397,17 +394,14 @@ func testBlueprintE(ctx context.Context, t *testing.T, client *Client) (*TwoStag } `json:"items"` } err = leafQuery.Do(ctx, &leafResponse) - if err != nil { - t.Fatal(errors.Join(err, bpDeleteFunc(ctx))) - } + require.NoError(t, err) + leafAssignements := make(SystemIdToInterfaceMapAssignment) for _, item := range leafResponse.Items { leafAssignements[item.Leaf.ID] = "Juniper_vQFX__AOS-7x10-Leaf" } err = bpClient.SetInterfaceMapAssignments(ctx, leafAssignements) - if err != nil { - t.Fatal(errors.Join(err, bpDeleteFunc(ctx))) - } + require.NoError(t, err) accessQuery := new(PathQuery). SetBlueprintId(bpId). @@ -427,19 +421,16 @@ func testBlueprintE(ctx context.Context, t *testing.T, client *Client) (*TwoStag } `json:"items"` } err = accessQuery.Do(ctx, &accessResponse) - if err != nil { - t.Fatal(errors.Join(err, bpDeleteFunc(ctx))) - } + require.NoError(t, err) + accessAssignements := make(SystemIdToInterfaceMapAssignment) for _, item := range accessResponse.Items { accessAssignements[item.Leaf.ID] = "Juniper_vQFX__AOS-8x10-1" } err = bpClient.SetInterfaceMapAssignments(ctx, accessAssignements) - if err != nil { - t.Fatal(errors.Join(err, bpDeleteFunc(ctx))) - } + require.NoError(t, err) - return bpClient, bpDeleteFunc + return bpClient } // testBlueprintH creates a test blueprint using client and returns a *TwoStageL3ClosClient. diff --git a/apstra/two_stage_l3_clos_redundancy_group_info.go b/apstra/two_stage_l3_clos_redundancy_group_info.go new file mode 100644 index 00000000..43b97bd6 --- /dev/null +++ b/apstra/two_stage_l3_clos_redundancy_group_info.go @@ -0,0 +1,118 @@ +package apstra + +import ( + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra/enum" +) + +type RedundancyGroupInfo struct { + Id ObjectId + Type enum.RgType + SystemType enum.SystemType + SystemRole enum.NodeRole + SystemIds [2]ObjectId +} + +func (o *TwoStageL3ClosClient) GetRedundancyGroupInfo(ctx context.Context, id ObjectId) (*RedundancyGroupInfo, error) { + resultMap, err := o.getRedundancyGroupInfo(ctx, id) + if err != nil { + return nil, err + } + + result, ok := resultMap[id] + if !ok { + return nil, ClientErr{ + errType: ErrNotfound, + err: fmt.Errorf("redundancy group %q not found", id), + } + } + + return &result, nil +} + +func (o *TwoStageL3ClosClient) GetAllRedundancyGroupInfo(ctx context.Context) (map[ObjectId]RedundancyGroupInfo, error) { + return o.getRedundancyGroupInfo(ctx, "") +} + +func (o *TwoStageL3ClosClient) getRedundancyGroupInfo(ctx context.Context, id ObjectId) (map[ObjectId]RedundancyGroupInfo, error) { + rgNodeAttrs := []QEEAttribute{ + NodeTypeRedundancyGroup.QEEAttribute(), + {Key: "name", Value: QEStringVal("n_redundancy_group")}, + } + if id != "" { + rgNodeAttrs = append(rgNodeAttrs, QEEAttribute{Key: "id", Value: QEStringVal(id)}) + } + + query := new(PathQuery). + SetBlueprintId(o.blueprintId). + SetClient(o.client). + Node(rgNodeAttrs). + Out([]QEEAttribute{RelationshipTypeComposedOfSystems.QEEAttribute()}). + Node([]QEEAttribute{NodeTypeSystem.QEEAttribute(), {Key: "name", Value: QEStringVal("n_system")}}) + + var queryResult struct { + Items []struct { + RedundancyGroup struct { + Id ObjectId `json:"id"` + Type enum.RgType `json:"rg_type"` + } `json:"n_redundancy_group"` + System struct { + Id ObjectId `json:"id"` + Role enum.NodeRole `json:"role"` + Type enum.SystemType `json:"system_type"` + } `json:"n_system"` + } `json:"items"` + } + + err := query.Do(ctx, &queryResult) + if err != nil { + return nil, fmt.Errorf("graph query %q failed - %w", query, err) + } + + result := make(map[ObjectId]RedundancyGroupInfo, len(queryResult.Items)/2) + for _, item := range queryResult.Items { + rgInfo, ok := result[item.RedundancyGroup.Id] + if !ok { + // create the map entry + result[item.RedundancyGroup.Id] = RedundancyGroupInfo{ + Id: item.RedundancyGroup.Id, + Type: item.RedundancyGroup.Type, + SystemType: item.System.Type, + SystemRole: item.System.Role, + SystemIds: [2]ObjectId{item.System.Id, ""}, + } + continue + } + + // validate the existing map entry + if rgInfo.Type != item.RedundancyGroup.Type { + return nil, fmt.Errorf("graph query %q returned inconsistent redundancy group types for group %q", query, item.RedundancyGroup.Id) + } + if rgInfo.SystemType != item.System.Type { + return nil, fmt.Errorf("graph query %q returned inconsistent system types for group %q", query, item.RedundancyGroup.Id) + } + if rgInfo.SystemRole != item.System.Role { + return nil, fmt.Errorf("graph query %q returned inconsistent system roles for group %q", query, item.RedundancyGroup.Id) + } + if rgInfo.SystemIds[1] != "" { + return nil, fmt.Errorf("graph query %q returned too many system nodes for redundancy group %q", query, item.RedundancyGroup.Id) + } + + // add the second system ID to the existing map entry + rgInfo.SystemIds[1] = item.System.Id + result[item.RedundancyGroup.Id] = rgInfo + } + + // ensure that each redundancy group has both system IDs + for k, v := range result { + if v.SystemIds[0] == "" || v.SystemIds[1] == "" { + return nil, ClientErr{ + errType: ErrInvalidApiResponse, + err: fmt.Errorf("graph query %q didn't find system pairs for redundancy group %q, got: %q", query, k, v), + } + } + } + + return result, nil +} diff --git a/apstra/two_stage_l3_clos_redundancy_group_info_integration_test.go b/apstra/two_stage_l3_clos_redundancy_group_info_integration_test.go new file mode 100644 index 00000000..9127a841 --- /dev/null +++ b/apstra/two_stage_l3_clos_redundancy_group_info_integration_test.go @@ -0,0 +1,76 @@ +package apstra + +import ( + "context" + "github.com/Juniper/apstra-go-sdk/apstra/enum" + "github.com/stretchr/testify/require" + "sort" + "testing" +) + +func TestGetRedundancyGroupInfo(t *testing.T) { + ctx := context.Background() + + clients, err := getTestClients(ctx, t) + require.NoError(t, err) + + compare := func(t *testing.T, a, b *RedundancyGroupInfo) { + t.Helper() + + require.NotNil(t, a) + require.NotNil(t, b) + require.Equal(t, a.Id, b.Id) + require.Equal(t, a.Type, b.Type) + require.Equal(t, a.SystemType, b.SystemType) + require.Equal(t, a.SystemRole, b.SystemRole) + + aSystemIds := a.SystemIds[:] + bSystemIds := b.SystemIds[:] + sort.Slice(aSystemIds, func(i, j int) bool { return aSystemIds[i] < aSystemIds[j] }) + sort.Slice(bSystemIds, func(i, j int) bool { return bSystemIds[i] < bSystemIds[j] }) + require.Equal(t, aSystemIds, bSystemIds) + } + + for _, client := range clients { + t.Run(client.name(), func(t *testing.T) { + t.Parallel() + + bp := testBlueprintE(ctx, t, client.client) + expectedAccessRgCount := 2 + expectedLeafRgCount := 2 + expectedTotalRgCount := expectedAccessRgCount + expectedLeafRgCount + + rgInfoMap, err := bp.GetAllRedundancyGroupInfo(ctx) + require.NoError(t, err) + require.Equal(t, expectedTotalRgCount, len(rgInfoMap)) // blueprint E has 4 RGs + + var accessRgCount, leafRgCount int + systemIds := make(map[ObjectId]struct{}, len(rgInfoMap)*2) + for k, v := range rgInfoMap { + require.Equal(t, k, v.Id) + require.Equal(t, enum.RgTypeEsi.String(), v.Type.String()) + require.Equal(t, enum.SystemTypeSwitch.String(), v.SystemType.String()) + + switch v.SystemRole.String() { + case enum.NodeRoleAccess.String(): + accessRgCount++ + case enum.NodeRoleLeaf.String(): + leafRgCount++ + } + + for _, id := range v.SystemIds { + systemIds[id] = struct{}{} + } + } + require.Equal(t, expectedTotalRgCount*2, len(systemIds)) + require.Equal(t, expectedAccessRgCount, accessRgCount) + require.Equal(t, expectedLeafRgCount, leafRgCount) + + for k, v := range rgInfoMap { + rgInfo, err := bp.GetRedundancyGroupInfo(ctx, k) + require.NoError(t, err) + compare(t, &v, rgInfo) + } + }) + } +}