From 6d7b9c3f4fcaf0a9bf8d7e6b36de2abbb3112ccc Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Fri, 5 Jul 2024 13:51:47 -0400 Subject: [PATCH 1/7] rename `apstra/utils/ct_ip_link_subinterfaces.go` to `apstra/utils/ct_ip_link.go` --- apstra/utils/{ct_ip_link_subinterfaces.go => ct_ip_link.go} | 0 .../{ct_ip_link_subinterfaces_test.go => ct_ip_link_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename apstra/utils/{ct_ip_link_subinterfaces.go => ct_ip_link.go} (100%) rename apstra/utils/{ct_ip_link_subinterfaces_test.go => ct_ip_link_test.go} (100%) diff --git a/apstra/utils/ct_ip_link_subinterfaces.go b/apstra/utils/ct_ip_link.go similarity index 100% rename from apstra/utils/ct_ip_link_subinterfaces.go rename to apstra/utils/ct_ip_link.go diff --git a/apstra/utils/ct_ip_link_subinterfaces_test.go b/apstra/utils/ct_ip_link_test.go similarity index 100% rename from apstra/utils/ct_ip_link_subinterfaces_test.go rename to apstra/utils/ct_ip_link_test.go From 05ddaeda1a545bf6c4a6828144747d24e157a2bf Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Fri, 5 Jul 2024 19:24:42 -0400 Subject: [PATCH 2/7] WIP: graph 1/2 graph query functions complete --- .../connectivity_template_assignments.go | 9 + apstra/utils/ct_ip_link.go | 163 ++++++++++++++++-- apstra/utils/ct_ip_link_test.go | 21 ++- 3 files changed, 174 insertions(+), 19 deletions(-) diff --git a/apstra/blueprint/connectivity_template_assignments.go b/apstra/blueprint/connectivity_template_assignments.go index 2ceadb45..b92ad160 100644 --- a/apstra/blueprint/connectivity_template_assignments.go +++ b/apstra/blueprint/connectivity_template_assignments.go @@ -17,6 +17,7 @@ type ConnectivityTemplateAssignments struct { BlueprintId types.String `tfsdk:"blueprint_id"` ConnectivityTemplateId types.String `tfsdk:"connectivity_template_id"` ApplicationPointIds types.Set `tfsdk:"application_point_ids"` + IpLinkIds types.Map `tfsdk:"ip_links_ids"` } func (o ConnectivityTemplateAssignments) ResourceAttributes() map[string]resourceSchema.Attribute { @@ -43,6 +44,14 @@ func (o ConnectivityTemplateAssignments) ResourceAttributes() map[string]resourc setvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)), }, }, + "ip_links_ids": resourceSchema.MapAttribute{ + MarkdownDescription: "New Logical Links are created when Connectivity Templates containing *IP Link* " + + "primitives are attached to a switch interface. These logical links may or may not be VLAN-tagged. " + + "This attribute is a two-dimensional map. The outer map is keyed by Application Point ID. The inner " + + "map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`.", + Computed: true, + ElementType: types.MapType{ElemType: types.StringType}, + }, } } diff --git a/apstra/utils/ct_ip_link.go b/apstra/utils/ct_ip_link.go index 9db950ad..0a23bff9 100644 --- a/apstra/utils/ct_ip_link.go +++ b/apstra/utils/ct_ip_link.go @@ -15,12 +15,12 @@ import ( // the Connectivity Template (top level ep_endpoint_policy object - the one used as "the CT ID" in // the web UI) and the Application Point (switch port) ID respectively. Subinterfaces of apId which // were not created by attaching ctId are ignored. -func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctId, apId apstra.ObjectId, diags *diag.Diagnostics) map[int64]apstra.ObjectId { +func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctId, apId string, diags *diag.Diagnostics) map[int64]apstra.ObjectId { ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(ctId)} apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(apId)} - iplNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ipl")} - iplNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} + iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} + iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} @@ -28,13 +28,13 @@ func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3Clos ctQuery := new(apstra.PathQuery). Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) - // query which identifies IP Link CT primitives - iplQuery := new(apstra.PathQuery). + // query which identifies IP Link primitives within the CT + iplpQuery := new(apstra.PathQuery). Node([]apstra.QEEAttribute{ctNodeNameAttr}). Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplNodeTypeAttr, iplNodeNameAttr}) + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) // query which identifies Subinterfaces siQuery := new(apstra.PathQuery). @@ -53,15 +53,15 @@ func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3Clos SetBlueprintId(client.Id()). SetClient(client.Client()). Match(ctQuery). - Match(iplQuery). + Match(iplpQuery). Match(siQuery) // collect the query response here var queryResponse struct { Items []struct { - IpL struct { + Iplp struct { Attributes json.RawMessage `json:"attributes"` - } `json:"n_ipl"` + } `json:"n_iplp"` Si struct { Id apstra.ObjectId `json:"id"` Vlan *int64 `json:"vlan_id"` @@ -91,9 +91,9 @@ func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3Clos // - mismatched combinations of IP Link primitive + subinterface for _, item := range queryResponse.Items { // un-quote the embedded JSON string - attributes, err := strconv.Unquote(string(item.IpL.Attributes)) + attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) if err != nil { - diags.AddError(fmt.Sprintf("failed to \"unquote\" IP Link attributes - '%s'", item.IpL.Attributes), err.Error()) + diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) return nil } @@ -122,3 +122,144 @@ func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3Clos return result } + +// GetCtIpLinkIdsByCtAndAps returns a two-dimensional map detailing the logical links created by +// attaching a connectivity template (ctId) containing IP Link CT primitives to switch ports (apIds). +// The map takes the form: map[ObjectId]map[int64]ObjectId +// +// Example: +// +// switch_port_one_id -> +// vlan_11 -> logical_link_foo_id +// vlan_12 -> logical_link_bar_id +// switch_port_two_id -> +// vlan_11 -> logical_link_baz_id +// vlan_12 -> logical_link_bang_id +// +// VLAN 0 indicates an untagged/native/null-VLAN logical link. +func GetCtIpLinkIdsByCtAndAps(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctId string, apIds []string, diags *diag.Diagnostics) map[apstra.ObjectId]map[int64]apstra.ObjectId { + ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(ctId)} + ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} + iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} + iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} + apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringValIsIn(apIds)} + apNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ap")} + siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} + siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} + llNodeTypeAttr := apstra.QEEAttribute{Key: "link_type", Value: apstra.QEStringVal("logical_link")} + llNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ll")} + + // query which identifies the Connectivity Template + ctQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) + + // query which identifies IP Link primitives within the CT + iplpQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) + + // query which identifies Subinterfaces + llQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr, apNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeLink.QEEAttribute(), llNodeTypeAttr, llNodeNameAttr}) + + // query which ties together the previous queries via `match()` + query := new(apstra.MatchQuery). + SetBlueprintId(client.Id()). + SetClient(client.Client()). + Match(ctQuery). + Match(iplpQuery). + Match(llQuery) + + // collect the query response here + var queryResponse struct { + Items []struct { + Iplp struct { + Attributes json.RawMessage `json:"attributes"` + } `json:"n_iplp"` + Ap struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ap"` + Si struct { + //Id apstra.ObjectId `json:"id"` + Vlan *int64 `json:"vlan_id"` + } `json:"n_si"` + Ll struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ll"` + } `json:"items"` + } + + err := query.Do(ctx, &queryResponse) + if err != nil { + diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) + return nil + } + + // the query result will include nested (and escaped) JSON. We'll unpack it here. + var ipLinkAttributes struct { + Vlan *int64 `json:"vlan_id"` + } + + // prepare the result map + result := make(map[apstra.ObjectId]map[int64]apstra.ObjectId) + + addToResult := func(apId apstra.ObjectId, vlan int64, linkId apstra.ObjectId) { + innerMap, ok := result[apId] + if !ok { + innerMap = make(map[int64]apstra.ObjectId) + } + innerMap[vlan] = linkId + result[apId] = innerMap + } + _ = addToResult + + for _, item := range queryResponse.Items { + // un-quote the embedded JSON string + attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) + if err != nil { + diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) + return nil + } + + // unpack the embedded JSON string + err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) + if err != nil { + diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) + return nil + } + + // if the IP Link Primitive matches the Subinterface, collect the result + switch { + case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: + // found the matching untagged IP Link and Subinterface - save as VLAN 0 + addToResult(item.Ap.Id, 0, item.Ll.Id) + continue + case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: + // one item is untagged, but the other is not - not interesting + continue + case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: + // IP link and subinterface are tagged - and they have matching values! + addToResult(item.Ap.Id, *ipLinkAttributes.Vlan, item.Ll.Id) + continue + } + } + + return result +} + +//func GetCtIpLinkIdsByApAndCts(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctIds []apstra.ObjectId, apId apstra.ObjectId, diags *diag.Diagnostics) map[int64]apstra.ObjectId { +//} +// diff --git a/apstra/utils/ct_ip_link_test.go b/apstra/utils/ct_ip_link_test.go index 90fb36f1..0e87a64b 100644 --- a/apstra/utils/ct_ip_link_test.go +++ b/apstra/utils/ct_ip_link_test.go @@ -2,6 +2,7 @@ package utils import ( "context" + "encoding/json" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stretchr/testify/require" @@ -9,28 +10,32 @@ import ( "testing" ) -func TestGetCtIpLinkSubinterfaces(t *testing.T) { +func TestThing(t *testing.T) { ctx := context.Background() clientCfg := apstra.ClientCfg{ - Url: "https://apstra-973cfb91-ecfb-4b46-8715-ce1849d7a041.aws.apstra.com", + Url: "https://apstra-2bd238e9-290d-4f8d-9f3a-05b501becd14.aws.apstra.com", User: "admin", - Pass: "CoolDuck3%", + Pass: "WillingMockingbird8-", } client, err := clientCfg.NewClient(ctx) require.NoError(t, err) - bp, err := client.NewTwoStageL3ClosClient(ctx, "e38a9808-6bbf-470d-82f1-f19e33d76c98") + bp, err := client.NewTwoStageL3ClosClient(ctx, "22044be2-e7af-462d-847a-ce6d0b49000e") require.NoError(t, err) var diags diag.Diagnostics - ctId := apstra.ObjectId("0bdf84ee-8129-4da6-8b35-c93c4febccce") - apId := apstra.ObjectId("k6L8SJevXl663_qqUlY") + ctId := "5765435a-acdd-46e7-81dc-a884dc4478ab" + //ctId := "d7d2daa1-4b15-4db6-8982-d4ff64a6a723" + apIds := []string{"IZiK1TY0amaN8UfU9Gc", "UVJpVynfmBMy74Nks4Y"} - vlanToSubinterfaces := GetCtIpLinkSubinterfaces(ctx, bp, ctId, apId, &diags) + vlanToSubinterfaces := GetCtIpLinkIdsByCtAndAps(ctx, bp, ctId, apIds, &diags) require.False(t, diags.HasError()) - log.Println(vlanToSubinterfaces) + m, err := json.MarshalIndent(vlanToSubinterfaces, "", " ") + require.NoError(t, err) + + log.Println(string(m)) } From c8f210a218f54de6939a5b0fe62a75eca016f23a Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Fri, 5 Jul 2024 21:11:47 -0400 Subject: [PATCH 3/7] add 2nd graph query function --- apstra/utils/ct_ip_link.go | 139 ++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/apstra/utils/ct_ip_link.go b/apstra/utils/ct_ip_link.go index 0a23bff9..478dc9e1 100644 --- a/apstra/utils/ct_ip_link.go +++ b/apstra/utils/ct_ip_link.go @@ -193,7 +193,6 @@ func GetCtIpLinkIdsByCtAndAps(ctx context.Context, client *apstra.TwoStageL3Clos Id apstra.ObjectId `json:"id"` } `json:"n_ap"` Si struct { - //Id apstra.ObjectId `json:"id"` Vlan *int64 `json:"vlan_id"` } `json:"n_si"` Ll struct { @@ -260,6 +259,140 @@ func GetCtIpLinkIdsByCtAndAps(ctx context.Context, client *apstra.TwoStageL3Clos return result } -//func GetCtIpLinkIdsByApAndCts(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctIds []apstra.ObjectId, apId apstra.ObjectId, diags *diag.Diagnostics) map[int64]apstra.ObjectId { -//} +// GetCtIpLinkIdsByApAndCts returns a two-dimensional map detailing the logical links created by +// attaching connectivity templates (ctIds) containing IP Link CT primitives to a switch port (apId). +// The map takes the form: map[ObjectId]map[int64]ObjectId +// +// Example: +// +// connectivity_template_one -> +// vlan_11 -> logical_link_foo_id +// vlan_12 -> logical_link_bar_id +// connectivity_template_one -> +// vlan_11 -> logical_link_baz_id +// vlan_12 -> logical_link_bang_id // +// VLAN 0 indicates an untagged/native/null-VLAN logical link. +func GetCtIpLinkIdsByApAndCts(ctx context.Context, client *apstra.TwoStageL3ClosClient, apId string, ctIds []string, diags *diag.Diagnostics) map[apstra.ObjectId]map[int64]apstra.ObjectId { + ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringValIsIn(ctIds)} + ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} + iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} + iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} + apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(apId)} + siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} + siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} + llNodeTypeAttr := apstra.QEEAttribute{Key: "link_type", Value: apstra.QEStringVal("logical_link")} + llNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ll")} + + // query which identifies the Connectivity Template + ctQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) + + // query which identifies IP Link primitives within the CT + iplpQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) + + // query which identifies Subinterfaces + llQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeLink.QEEAttribute(), llNodeTypeAttr, llNodeNameAttr}) + + // query which ties together the previous queries via `match()` + query := new(apstra.MatchQuery). + SetBlueprintId(client.Id()). + SetClient(client.Client()). + Match(ctQuery). + Match(iplpQuery). + Match(llQuery) + + // collect the query response here + var queryResponse struct { + Items []struct { + Ct struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ct"` + Iplp struct { + Attributes json.RawMessage `json:"attributes"` + } `json:"n_iplp"` + //Ap struct { + // Id apstra.ObjectId `json:"id"` + //} `json:"n_ap"` + Si struct { + Vlan *int64 `json:"vlan_id"` + } `json:"n_si"` + Ll struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ll"` + } `json:"items"` + } + + err := query.Do(ctx, &queryResponse) + if err != nil { + diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) + return nil + } + + // the query result will include nested (and escaped) JSON. We'll unpack it here. + var ipLinkAttributes struct { + Vlan *int64 `json:"vlan_id"` + } + + // prepare the result map + result := make(map[apstra.ObjectId]map[int64]apstra.ObjectId) + + addToResult := func(ctId apstra.ObjectId, vlan int64, linkId apstra.ObjectId) { + innerMap, ok := result[ctId] + if !ok { + innerMap = make(map[int64]apstra.ObjectId) + } + innerMap[vlan] = linkId + result[ctId] = innerMap + } + _ = addToResult + + for _, item := range queryResponse.Items { + // un-quote the embedded JSON string + attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) + if err != nil { + diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) + return nil + } + + // unpack the embedded JSON string + err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) + if err != nil { + diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) + return nil + } + + // if the IP Link Primitive matches the Subinterface, collect the result + switch { + case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: + // found the matching untagged IP Link and Subinterface - save as VLAN 0 + addToResult(item.Ct.Id, 0, item.Ll.Id) + continue + case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: + // one item is untagged, but the other is not - not interesting + continue + case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: + // IP link and subinterface are tagged - and they have matching values! + addToResult(item.Ct.Id, *ipLinkAttributes.Vlan, item.Ll.Id) + continue + } + } + + return result +} From ad81e22275cc451dd4d84e9903dc25c2dc4cbbe8 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Fri, 5 Jul 2024 23:38:29 -0400 Subject: [PATCH 4/7] add ip_link_ids attribute to apstra_datacenter_connectivity_template_assignments resource --- .../connectivity_template_assignments.go | 150 +++++++++++++++++- ...enter_connectivity_template_assignments.go | 19 ++- apstra/utils/ct_ip_link_test.go | 33 ++++ ...enter_connectivity_template_assignments.md | 9 ++ 4 files changed, 208 insertions(+), 3 deletions(-) diff --git a/apstra/blueprint/connectivity_template_assignments.go b/apstra/blueprint/connectivity_template_assignments.go index b92ad160..2c7357df 100644 --- a/apstra/blueprint/connectivity_template_assignments.go +++ b/apstra/blueprint/connectivity_template_assignments.go @@ -2,6 +2,10 @@ package blueprint import ( "context" + "encoding/json" + "fmt" + "strconv" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -17,6 +21,7 @@ type ConnectivityTemplateAssignments struct { BlueprintId types.String `tfsdk:"blueprint_id"` ConnectivityTemplateId types.String `tfsdk:"connectivity_template_id"` ApplicationPointIds types.Set `tfsdk:"application_point_ids"` + FetchIpLinkIds types.Bool `tfsdk:"fetch_ip_link_ids"` IpLinkIds types.Map `tfsdk:"ip_links_ids"` } @@ -44,11 +49,18 @@ func (o ConnectivityTemplateAssignments) ResourceAttributes() map[string]resourc setvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)), }, }, + "fetch_ip_link_ids": resourceSchema.BoolAttribute{ + MarkdownDescription: "When `true`, the read-only `ip_link_ids` attribute will be populated. Default " + + "behavior skips retrieving `ip_link_ids` to improve performance in scenarios where this information " + + "is not needed.", + Optional: true, + }, "ip_links_ids": resourceSchema.MapAttribute{ MarkdownDescription: "New Logical Links are created when Connectivity Templates containing *IP Link* " + "primitives are attached to a switch interface. These logical links may or may not be VLAN-tagged. " + "This attribute is a two-dimensional map. The outer map is keyed by Application Point ID. The inner " + - "map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`.", + "map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`.\n" + + "**Note:** requires `fetch_iplink_ids = true`", Computed: true, ElementType: types.MapType{ElemType: types.StringType}, }, @@ -97,3 +109,139 @@ func (o *ConnectivityTemplateAssignments) Request(ctx context.Context, state *Co return result } + +func (o *ConnectivityTemplateAssignments) GetIpLinkIds(ctx context.Context, bp *apstra.TwoStageL3ClosClient, diags *diag.Diagnostics) { + o.IpLinkIds = types.MapNull(types.MapType{ElemType: types.StringType}) + + if !o.FetchIpLinkIds.ValueBool() { + return + } + + var applicationPointIds []string + diags.Append(o.ApplicationPointIds.ElementsAs(ctx, &applicationPointIds, false)...) + if diags.HasError() { + return + } + + ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(o.ConnectivityTemplateId.ValueString())} + ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} + iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} + iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} + apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringValIsIn(applicationPointIds)} + apNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ap")} + siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} + siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} + llNodeTypeAttr := apstra.QEEAttribute{Key: "link_type", Value: apstra.QEStringVal("logical_link")} + llNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ll")} + + // query which identifies the Connectivity Template + ctQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) + + // query which identifies IP Link primitives within the CT + iplpQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) + + // query which identifies Subinterfaces + llQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr, apNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeLink.QEEAttribute(), llNodeTypeAttr, llNodeNameAttr}) + + // query which ties together the previous queries via `match()` + query := new(apstra.MatchQuery). + SetBlueprintId(bp.Id()). + SetClient(bp.Client()). + Match(ctQuery). + Match(iplpQuery). + Match(llQuery) + + // collect the query response here + var queryResponse struct { + Items []struct { + Iplp struct { + Attributes json.RawMessage `json:"attributes"` + } `json:"n_iplp"` + Ap struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ap"` + Si struct { + Vlan *int `json:"vlan_id"` + } `json:"n_si"` + Ll struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ll"` + } `json:"items"` + } + + err := query.Do(ctx, &queryResponse) + if err != nil { + diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) + return + } + + // the query result will include nested (and escaped) JSON. We'll unpack it here. + var ipLinkAttributes struct { + Vlan *int `json:"vlan_id"` + } + + // prepare the result map + result := make(map[apstra.ObjectId]map[string]apstra.ObjectId) + + addToResult := func(apId apstra.ObjectId, vlan int, linkId apstra.ObjectId) { + innerMap, ok := result[apId] + if !ok { + innerMap = make(map[string]apstra.ObjectId) + } + innerMap[strconv.Itoa(vlan)] = linkId + result[apId] = innerMap + } + _ = addToResult + + for _, item := range queryResponse.Items { + // un-quote the embedded JSON string + attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) + if err != nil { + diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) + return + } + + // unpack the embedded JSON string + err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) + if err != nil { + diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) + return + } + + // if the IP Link Primitive matches the Subinterface, collect the result + switch { + case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: + // found the matching untagged IP Link and Subinterface - save as VLAN 0 + addToResult(item.Ap.Id, 0, item.Ll.Id) + continue + case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: + // one item is untagged, but the other is not - not interesting + continue + case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: + // IP link and subinterface are tagged - and they have matching values! + addToResult(item.Ap.Id, *ipLinkAttributes.Vlan, item.Ll.Id) + continue + } + } + + var d diag.Diagnostics + o.IpLinkIds, d = types.MapValueFrom(ctx, types.MapType{ElemType: types.StringType}, result) + diags.Append(d...) +} diff --git a/apstra/resource_datacenter_connectivity_template_assignments.go b/apstra/resource_datacenter_connectivity_template_assignments.go index d3d4747c..3494dedd 100644 --- a/apstra/resource_datacenter_connectivity_template_assignments.go +++ b/apstra/resource_datacenter_connectivity_template_assignments.go @@ -82,7 +82,13 @@ func (o *resourceDatacenterConnectivityTemplateAssignments) Create(ctx context.C err.Error()) } - // set state + // Fetch IP link IDs + plan.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } @@ -123,8 +129,13 @@ func (o *resourceDatacenterConnectivityTemplateAssignments) Read(ctx context.Con } } - // Set state + // Load application point IDs state.ApplicationPointIds = types.SetValueMust(types.StringType, apIds) + + // Fetch IP link IDs + state.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + + // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -171,6 +182,10 @@ func (o *resourceDatacenterConnectivityTemplateAssignments) Update(ctx context.C err.Error()) } + // Fetch IP link IDs + plan.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + + // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } diff --git a/apstra/utils/ct_ip_link_test.go b/apstra/utils/ct_ip_link_test.go index 0e87a64b..3f97d6cd 100644 --- a/apstra/utils/ct_ip_link_test.go +++ b/apstra/utils/ct_ip_link_test.go @@ -39,3 +39,36 @@ func TestThing(t *testing.T) { log.Println(string(m)) } + +func TestThing2(t *testing.T) { + ctx := context.Background() + + clientCfg := apstra.ClientCfg{ + Url: "https://apstra-2bd238e9-290d-4f8d-9f3a-05b501becd14.aws.apstra.com", + User: "admin", + Pass: "WillingMockingbird8-", + } + + client, err := clientCfg.NewClient(ctx) + require.NoError(t, err) + + bp, err := client.NewTwoStageL3ClosClient(ctx, "22044be2-e7af-462d-847a-ce6d0b49000e") + require.NoError(t, err) + + var diags diag.Diagnostics + + apId := "IZiK1TY0amaN8UfU9Gc" + ctIds := []string{ + //"5765435a-acdd-46e7-81dc-a884dc4478ab", + //"d7d2daa1-4b15-4db6-8982-d4ff64a6a723", + //"2d2cb4f3-9603-4daa-83c1-5c0f142bb6da", + } + + vlanToSubinterfaces := GetCtIpLinkIdsByApAndCts(ctx, bp, apId, ctIds, &diags) + require.False(t, diags.HasError()) + + m, err := json.MarshalIndent(vlanToSubinterfaces, "", " ") + require.NoError(t, err) + + log.Println(string(m)) +} diff --git a/docs/resources/datacenter_connectivity_template_assignments.md b/docs/resources/datacenter_connectivity_template_assignments.md index bec3f85f..ce39fca0 100644 --- a/docs/resources/datacenter_connectivity_template_assignments.md +++ b/docs/resources/datacenter_connectivity_template_assignments.md @@ -45,5 +45,14 @@ resource "apstra_datacenter_connectivity_template_assignments" "a" { - `blueprint_id` (String) Apstra Blueprint ID. - `connectivity_template_id` (String) Connectivity Template ID which should be applied to the Application Points. +### Optional + +- `fetch_ip_link_ids` (Boolean) When `true`, the read-only `ip_link_ids` attribute will be populated. Default behavior skips retrieving `ip_link_ids` to improve performance in scenarios where this information is not needed. + +### Read-Only + +- `ip_links_ids` (Map of Map of String) New Logical Links are created when Connectivity Templates containing *IP Link* primitives are attached to a switch interface. These logical links may or may not be VLAN-tagged. This attribute is a two-dimensional map. The outer map is keyed by Application Point ID. The inner map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`. +**Note:** requires `fetch_iplink_ids = true` + From 0cfc8707e7669d47406d6f60c778507c65ddd9b4 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Sat, 6 Jul 2024 17:44:25 -0400 Subject: [PATCH 5/7] add `ip_link_ids` attribute to `apstra_datacenter_connectivity_templates_assignment` resource --- apstra/blueprint/connectivity_template.go | 1 + .../connectivity_templates_assignment.go | 160 ++++++++++++++++++ ...center_connectivity_template_assignment.go | 9 +- ...enter_connectivity_template_assignments.go | 6 + ...enter_connectivity_templates_assignment.go | 27 ++- 5 files changed, 197 insertions(+), 6 deletions(-) diff --git a/apstra/blueprint/connectivity_template.go b/apstra/blueprint/connectivity_template.go index 5a93e0e8..4a7ee288 100644 --- a/apstra/blueprint/connectivity_template.go +++ b/apstra/blueprint/connectivity_template.go @@ -2,6 +2,7 @@ package blueprint import ( "context" + "github.com/Juniper/apstra-go-sdk/apstra" connectivitytemplate "github.com/Juniper/terraform-provider-apstra/apstra/connectivity_template" "github.com/Juniper/terraform-provider-apstra/apstra/utils" diff --git a/apstra/blueprint/connectivity_templates_assignment.go b/apstra/blueprint/connectivity_templates_assignment.go index b0b13232..136fd75d 100644 --- a/apstra/blueprint/connectivity_templates_assignment.go +++ b/apstra/blueprint/connectivity_templates_assignment.go @@ -2,6 +2,10 @@ package blueprint import ( "context" + "encoding/json" + "fmt" + "strconv" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -18,6 +22,8 @@ type ConnectivityTemplatesAssignment struct { BlueprintId types.String `tfsdk:"blueprint_id"` ConnectivityTemplateIds types.Set `tfsdk:"connectivity_template_ids"` ApplicationPointId types.String `tfsdk:"application_point_id"` + FetchIpLinkIds types.Bool `tfsdk:"fetch_ip_link_ids"` + IpLinkIds types.Map `tfsdk:"ip_links_ids"` } func (o ConnectivityTemplatesAssignment) ResourceAttributes() map[string]resourceSchema.Attribute { @@ -44,6 +50,21 @@ func (o ConnectivityTemplatesAssignment) ResourceAttributes() map[string]resourc setvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)), }, }, + "fetch_ip_link_ids": resourceSchema.BoolAttribute{ + MarkdownDescription: "When `true`, the read-only `ip_link_ids` attribute will be populated. Default " + + "behavior skips retrieving `ip_link_ids` to improve performance in scenarios where this information " + + "is not needed.", + Optional: true, + }, + "ip_links_ids": resourceSchema.MapAttribute{ + MarkdownDescription: "New Logical Links are created when Connectivity Templates containing *IP Link* " + + "primitives are attached to a switch interface. These logical links may or may not be VLAN-tagged. " + + "This attribute is a two-dimensional map. The outer map is keyed by Connectivity Template ID. The inner " + + "map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`.\n" + + "**Note:** requires `fetch_iplink_ids = true`", + Computed: true, + ElementType: types.MapType{ElemType: types.StringType}, + }, } } @@ -66,3 +87,142 @@ func (o *ConnectivityTemplatesAssignment) AddDelRequest(ctx context.Context, sta return utils.SliceComplementOfA(stateIds, planIds), utils.SliceComplementOfA(planIds, stateIds) } + +func (o *ConnectivityTemplatesAssignment) GetIpLinkIds(ctx context.Context, bp *apstra.TwoStageL3ClosClient, diags *diag.Diagnostics) { + o.IpLinkIds = types.MapNull(types.MapType{ElemType: types.StringType}) + + if !o.FetchIpLinkIds.ValueBool() { + return + } + + var connectivityTemplateIds []string + diags.Append(o.ConnectivityTemplateIds.ElementsAs(ctx, &connectivityTemplateIds, false)...) + if diags.HasError() { + return + } + + ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringValIsIn(connectivityTemplateIds)} + ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} + iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} + iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} + apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(o.ApplicationPointId.ValueString())} + apNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ap")} + siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} + siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} + llNodeTypeAttr := apstra.QEEAttribute{Key: "link_type", Value: apstra.QEStringVal("logical_link")} + llNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ll")} + + // query which identifies the Connectivity Template + ctQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) + + // query which identifies IP Link primitives within the CT + iplpQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) + + // query which identifies Subinterfaces + llQuery := new(apstra.PathQuery). + Node([]apstra.QEEAttribute{ctNodeNameAttr}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr, apNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeLink.QEEAttribute(), llNodeTypeAttr, llNodeNameAttr}) + + // query which ties together the previous queries via `match()` + query := new(apstra.MatchQuery). + SetBlueprintId(bp.Id()). + SetClient(bp.Client()). + Match(ctQuery). + Match(iplpQuery). + Match(llQuery) + + // collect the query response here + var queryResponse struct { + Items []struct { + Ct struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ct"` + Iplp struct { + Attributes json.RawMessage `json:"attributes"` + } `json:"n_iplp"` + //Ap struct { + // Id apstra.ObjectId `json:"id"` + //} `json:"n_ap"` + Si struct { + Vlan *int `json:"vlan_id"` + } `json:"n_si"` + Ll struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_ll"` + } `json:"items"` + } + + err := query.Do(ctx, &queryResponse) + if err != nil { + diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) + return + } + + // the query result will include nested (and escaped) JSON. We'll unpack it here. + var ipLinkAttributes struct { + Vlan *int `json:"vlan_id"` + } + + // prepare the result map + result := make(map[apstra.ObjectId]map[string]apstra.ObjectId) + + addToResult := func(ctId apstra.ObjectId, vlan int, linkId apstra.ObjectId) { + innerMap, ok := result[ctId] + if !ok { + innerMap = make(map[string]apstra.ObjectId) + } + innerMap[strconv.Itoa(vlan)] = linkId + result[ctId] = innerMap + } + _ = addToResult + + for _, item := range queryResponse.Items { + // un-quote the embedded JSON string + attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) + if err != nil { + diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) + return + } + + // unpack the embedded JSON string + err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) + if err != nil { + diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) + return + } + + // if the IP Link Primitive matches the Subinterface, collect the result + switch { + case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: + // found the matching untagged IP Link and Subinterface - save as VLAN 0 + addToResult(item.Ct.Id, 0, item.Ll.Id) + continue + case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: + // one item is untagged, but the other is not - not interesting + continue + case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: + // IP link and subinterface are tagged - and they have matching values! + addToResult(item.Ct.Id, *ipLinkAttributes.Vlan, item.Ll.Id) + continue + } + } + + var d diag.Diagnostics + o.IpLinkIds, d = types.MapValueFrom(ctx, types.MapType{ElemType: types.StringType}, result) + diags.Append(d...) +} diff --git a/apstra/resource_datacenter_connectivity_template_assignment.go b/apstra/resource_datacenter_connectivity_template_assignment.go index 6c25d7bb..14baa54e 100644 --- a/apstra/resource_datacenter_connectivity_template_assignment.go +++ b/apstra/resource_datacenter_connectivity_template_assignment.go @@ -3,6 +3,7 @@ package tfapstra import ( "context" "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" "github.com/Juniper/terraform-provider-apstra/apstra/utils" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplateAssignment{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplateAssignment{} -var _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplateAssignment{} +var ( + _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplateAssignment{} + _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplateAssignment{} + _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplateAssignment{} +) type resourceDatacenterConnectivityTemplateAssignment struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) diff --git a/apstra/resource_datacenter_connectivity_template_assignments.go b/apstra/resource_datacenter_connectivity_template_assignments.go index 3494dedd..af4e2968 100644 --- a/apstra/resource_datacenter_connectivity_template_assignments.go +++ b/apstra/resource_datacenter_connectivity_template_assignments.go @@ -134,6 +134,9 @@ func (o *resourceDatacenterConnectivityTemplateAssignments) Read(ctx context.Con // Fetch IP link IDs state.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -184,6 +187,9 @@ func (o *resourceDatacenterConnectivityTemplateAssignments) Update(ctx context.C // Fetch IP link IDs plan.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) diff --git a/apstra/resource_datacenter_connectivity_templates_assignment.go b/apstra/resource_datacenter_connectivity_templates_assignment.go index 0665c1f8..1d422e85 100644 --- a/apstra/resource_datacenter_connectivity_templates_assignment.go +++ b/apstra/resource_datacenter_connectivity_templates_assignment.go @@ -3,6 +3,7 @@ package tfapstra import ( "context" "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" "github.com/Juniper/terraform-provider-apstra/apstra/utils" @@ -11,9 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplatesAssignment{} -var _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplatesAssignment{} -var _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplatesAssignment{} +var ( + _ resource.ResourceWithConfigure = &resourceDatacenterConnectivityTemplatesAssignment{} + _ resourceWithSetBpClientFunc = &resourceDatacenterConnectivityTemplatesAssignment{} + _ resourceWithSetBpLockFunc = &resourceDatacenterConnectivityTemplatesAssignment{} +) type resourceDatacenterConnectivityTemplatesAssignment struct { getBpClientFunc func(context.Context, string) (*apstra.TwoStageL3ClosClient, error) @@ -72,6 +75,12 @@ func (o *resourceDatacenterConnectivityTemplatesAssignment) Create(ctx context.C return } + // Fetch IP link IDs + plan.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } @@ -119,6 +128,12 @@ func (o *resourceDatacenterConnectivityTemplatesAssignment) Read(ctx context.Con return } + // Fetch IP link IDs + state.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -174,6 +189,12 @@ func (o *resourceDatacenterConnectivityTemplatesAssignment) Update(ctx context.C return } + // Fetch IP link IDs + plan.GetIpLinkIds(ctx, bp, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } From 83cc4ddb53b06865cf2e7294861bb0f9fe8f3003 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Sat, 6 Jul 2024 17:44:47 -0400 Subject: [PATCH 6/7] make docs --- .../datacenter_connectivity_template_assignment.md | 9 +++++++++ .../datacenter_connectivity_templates_assignment.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/resources/datacenter_connectivity_template_assignment.md b/docs/resources/datacenter_connectivity_template_assignment.md index 5a93f229..2122e77b 100644 --- a/docs/resources/datacenter_connectivity_template_assignment.md +++ b/docs/resources/datacenter_connectivity_template_assignment.md @@ -47,5 +47,14 @@ resource "apstra_datacenter_connectivity_template_assignment" "a" { - `blueprint_id` (String) Apstra Blueprint ID. - `connectivity_template_ids` (Set of String) Set of Connectivity Template IDs which should be applied to the Application Point. +### Optional + +- `fetch_ip_link_ids` (Boolean) When `true`, the read-only `ip_link_ids` attribute will be populated. Default behavior skips retrieving `ip_link_ids` to improve performance in scenarios where this information is not needed. + +### Read-Only + +- `ip_links_ids` (Map of Map of String) New Logical Links are created when Connectivity Templates containing *IP Link* primitives are attached to a switch interface. These logical links may or may not be VLAN-tagged. This attribute is a two-dimensional map. The outer map is keyed by Connectivity Template ID. The inner map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`. +**Note:** requires `fetch_iplink_ids = true` + diff --git a/docs/resources/datacenter_connectivity_templates_assignment.md b/docs/resources/datacenter_connectivity_templates_assignment.md index 49e1e002..9ccac903 100644 --- a/docs/resources/datacenter_connectivity_templates_assignment.md +++ b/docs/resources/datacenter_connectivity_templates_assignment.md @@ -41,5 +41,14 @@ resource "apstra_datacenter_connectivity_template_assignment" "a" { - `blueprint_id` (String) Apstra Blueprint ID. - `connectivity_template_ids` (Set of String) Set of Connectivity Template IDs which should be applied to the Application Point. +### Optional + +- `fetch_ip_link_ids` (Boolean) When `true`, the read-only `ip_link_ids` attribute will be populated. Default behavior skips retrieving `ip_link_ids` to improve performance in scenarios where this information is not needed. + +### Read-Only + +- `ip_links_ids` (Map of Map of String) New Logical Links are created when Connectivity Templates containing *IP Link* primitives are attached to a switch interface. These logical links may or may not be VLAN-tagged. This attribute is a two-dimensional map. The outer map is keyed by Connectivity Template ID. The inner map is keyed by VLAN number. Untagged Logical Links are represented in the inner map by key `0`. +**Note:** requires `fetch_iplink_ids = true` + From d14541e6768cf41231e3947d6acd09d71b2eaa55 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Sat, 6 Jul 2024 17:46:15 -0400 Subject: [PATCH 7/7] remove files where ip_link_ids functions were developed --- apstra/utils/ct_ip_link.go | 398 -------------------------------- apstra/utils/ct_ip_link_test.go | 74 ------ 2 files changed, 472 deletions(-) delete mode 100644 apstra/utils/ct_ip_link.go delete mode 100644 apstra/utils/ct_ip_link_test.go diff --git a/apstra/utils/ct_ip_link.go b/apstra/utils/ct_ip_link.go deleted file mode 100644 index 478dc9e1..00000000 --- a/apstra/utils/ct_ip_link.go +++ /dev/null @@ -1,398 +0,0 @@ -package utils - -import ( - "context" - "encoding/json" - "fmt" - "github.com/Juniper/apstra-go-sdk/apstra" - "github.com/hashicorp/terraform-plugin-framework/diag" - "strconv" -) - -// GetCtIpLinkSubinterfaces returns a map of switch subinterfaces created by attaching a Connectivity -// Template with top-level "IP Link" (AttachLogialLInk) Primitivves keyed by VLAN number. Key 0 -// represents the "no tag" condition. Inputs ctId and apId are both graph node IDs. They represent -// the Connectivity Template (top level ep_endpoint_policy object - the one used as "the CT ID" in -// the web UI) and the Application Point (switch port) ID respectively. Subinterfaces of apId which -// were not created by attaching ctId are ignored. -func GetCtIpLinkSubinterfaces(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctId, apId string, diags *diag.Diagnostics) map[int64]apstra.ObjectId { - ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} - ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(ctId)} - apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(apId)} - iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} - iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} - siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} - siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} - - // query which identifies the Connectivity Template - ctQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) - - // query which identifies IP Link primitives within the CT - iplpQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{ctNodeNameAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) - - // query which identifies Subinterfaces - siQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{ctNodeNameAttr}). - In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). - In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}) - - // query which ties together the previous queries via `match()` - query := new(apstra.MatchQuery). - SetBlueprintId(client.Id()). - SetClient(client.Client()). - Match(ctQuery). - Match(iplpQuery). - Match(siQuery) - - // collect the query response here - var queryResponse struct { - Items []struct { - Iplp struct { - Attributes json.RawMessage `json:"attributes"` - } `json:"n_iplp"` - Si struct { - Id apstra.ObjectId `json:"id"` - Vlan *int64 `json:"vlan_id"` - } `json:"n_si"` - } `json:"items"` - } - - // execute the query - err := query.Do(ctx, &queryResponse) - if err != nil { - diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) - return nil - } - - // the query result will include nested (and escaped) JSON. We'll unpack it here. - var ipLinkAttributes struct { - Vlan *int64 `json:"vlan_id"` - } - - // prepare the result map - result := make(map[int64]apstra.ObjectId) - - // iterate over the query response inspecting each item. - // note that result will not necessarily be sized the same as queryResponse.Items - // because the graph traversal can find multiple extraneous traversals: - // - subinterfaces not related to the Connectivity Template - // - mismatched combinations of IP Link primitive + subinterface - for _, item := range queryResponse.Items { - // un-quote the embedded JSON string - attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) - if err != nil { - diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) - return nil - } - - // unpack the embedded JSON string - err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) - if err != nil { - diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) - return nil - } - - // if the IP Link Primitive matches the Subinterface, collect the result - switch { - case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: - // found the matching untagged IP Link and Subinterface - save as VLAN 0 - result[0] = item.Si.Id - continue - case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: - // one item is untagged, but the other is not - not interesting - continue - case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: - // IP link and subinterface are tagged - and they have matching values! - result[*ipLinkAttributes.Vlan] = item.Si.Id - continue - } - } - - return result -} - -// GetCtIpLinkIdsByCtAndAps returns a two-dimensional map detailing the logical links created by -// attaching a connectivity template (ctId) containing IP Link CT primitives to switch ports (apIds). -// The map takes the form: map[ObjectId]map[int64]ObjectId -// -// Example: -// -// switch_port_one_id -> -// vlan_11 -> logical_link_foo_id -// vlan_12 -> logical_link_bar_id -// switch_port_two_id -> -// vlan_11 -> logical_link_baz_id -// vlan_12 -> logical_link_bang_id -// -// VLAN 0 indicates an untagged/native/null-VLAN logical link. -func GetCtIpLinkIdsByCtAndAps(ctx context.Context, client *apstra.TwoStageL3ClosClient, ctId string, apIds []string, diags *diag.Diagnostics) map[apstra.ObjectId]map[int64]apstra.ObjectId { - ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(ctId)} - ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} - iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} - iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} - apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringValIsIn(apIds)} - apNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ap")} - siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} - siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} - llNodeTypeAttr := apstra.QEEAttribute{Key: "link_type", Value: apstra.QEStringVal("logical_link")} - llNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ll")} - - // query which identifies the Connectivity Template - ctQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) - - // query which identifies IP Link primitives within the CT - iplpQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{ctNodeNameAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) - - // query which identifies Subinterfaces - llQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{ctNodeNameAttr}). - In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). - In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr, apNodeNameAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeLink.QEEAttribute(), llNodeTypeAttr, llNodeNameAttr}) - - // query which ties together the previous queries via `match()` - query := new(apstra.MatchQuery). - SetBlueprintId(client.Id()). - SetClient(client.Client()). - Match(ctQuery). - Match(iplpQuery). - Match(llQuery) - - // collect the query response here - var queryResponse struct { - Items []struct { - Iplp struct { - Attributes json.RawMessage `json:"attributes"` - } `json:"n_iplp"` - Ap struct { - Id apstra.ObjectId `json:"id"` - } `json:"n_ap"` - Si struct { - Vlan *int64 `json:"vlan_id"` - } `json:"n_si"` - Ll struct { - Id apstra.ObjectId `json:"id"` - } `json:"n_ll"` - } `json:"items"` - } - - err := query.Do(ctx, &queryResponse) - if err != nil { - diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) - return nil - } - - // the query result will include nested (and escaped) JSON. We'll unpack it here. - var ipLinkAttributes struct { - Vlan *int64 `json:"vlan_id"` - } - - // prepare the result map - result := make(map[apstra.ObjectId]map[int64]apstra.ObjectId) - - addToResult := func(apId apstra.ObjectId, vlan int64, linkId apstra.ObjectId) { - innerMap, ok := result[apId] - if !ok { - innerMap = make(map[int64]apstra.ObjectId) - } - innerMap[vlan] = linkId - result[apId] = innerMap - } - _ = addToResult - - for _, item := range queryResponse.Items { - // un-quote the embedded JSON string - attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) - if err != nil { - diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) - return nil - } - - // unpack the embedded JSON string - err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) - if err != nil { - diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) - return nil - } - - // if the IP Link Primitive matches the Subinterface, collect the result - switch { - case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: - // found the matching untagged IP Link and Subinterface - save as VLAN 0 - addToResult(item.Ap.Id, 0, item.Ll.Id) - continue - case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: - // one item is untagged, but the other is not - not interesting - continue - case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: - // IP link and subinterface are tagged - and they have matching values! - addToResult(item.Ap.Id, *ipLinkAttributes.Vlan, item.Ll.Id) - continue - } - } - - return result -} - -// GetCtIpLinkIdsByApAndCts returns a two-dimensional map detailing the logical links created by -// attaching connectivity templates (ctIds) containing IP Link CT primitives to a switch port (apId). -// The map takes the form: map[ObjectId]map[int64]ObjectId -// -// Example: -// -// connectivity_template_one -> -// vlan_11 -> logical_link_foo_id -// vlan_12 -> logical_link_bar_id -// connectivity_template_one -> -// vlan_11 -> logical_link_baz_id -// vlan_12 -> logical_link_bang_id -// -// VLAN 0 indicates an untagged/native/null-VLAN logical link. -func GetCtIpLinkIdsByApAndCts(ctx context.Context, client *apstra.TwoStageL3ClosClient, apId string, ctIds []string, diags *diag.Diagnostics) map[apstra.ObjectId]map[int64]apstra.ObjectId { - ctNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringValIsIn(ctIds)} - ctNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ct")} - iplpNodeTypeAttr := apstra.QEEAttribute{Key: "policy_type_name", Value: apstra.QEStringVal("AttachLogicalLink")} - iplpNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_iplp")} - apNodeIdAttr := apstra.QEEAttribute{Key: "id", Value: apstra.QEStringVal(apId)} - siNodeTypeAttr := apstra.QEEAttribute{Key: "if_type", Value: apstra.QEStringVal("subinterface")} - siNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_si")} - llNodeTypeAttr := apstra.QEEAttribute{Key: "link_type", Value: apstra.QEStringVal("logical_link")} - llNodeNameAttr := apstra.QEEAttribute{Key: "name", Value: apstra.QEStringVal("n_ll")} - - // query which identifies the Connectivity Template - ctQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), ctNodeIdAttr, ctNodeNameAttr}) - - // query which identifies IP Link primitives within the CT - iplpQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{ctNodeNameAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute()}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpFirstSubpolicy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpEndpointPolicy.QEEAttribute(), iplpNodeTypeAttr, iplpNodeNameAttr}) - - // query which identifies Subinterfaces - llQuery := new(apstra.PathQuery). - Node([]apstra.QEEAttribute{ctNodeNameAttr}). - In([]apstra.QEEAttribute{apstra.RelationshipTypeEpNested.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpApplicationInstance.QEEAttribute()}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeEpAffectedBy.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeEpGroup.QEEAttribute()}). - In([]apstra.QEEAttribute{apstra.RelationshipTypeEpMemberOf.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), apNodeIdAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOf.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeInterface.QEEAttribute(), siNodeTypeAttr, siNodeNameAttr}). - Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}). - Node([]apstra.QEEAttribute{apstra.NodeTypeLink.QEEAttribute(), llNodeTypeAttr, llNodeNameAttr}) - - // query which ties together the previous queries via `match()` - query := new(apstra.MatchQuery). - SetBlueprintId(client.Id()). - SetClient(client.Client()). - Match(ctQuery). - Match(iplpQuery). - Match(llQuery) - - // collect the query response here - var queryResponse struct { - Items []struct { - Ct struct { - Id apstra.ObjectId `json:"id"` - } `json:"n_ct"` - Iplp struct { - Attributes json.RawMessage `json:"attributes"` - } `json:"n_iplp"` - //Ap struct { - // Id apstra.ObjectId `json:"id"` - //} `json:"n_ap"` - Si struct { - Vlan *int64 `json:"vlan_id"` - } `json:"n_si"` - Ll struct { - Id apstra.ObjectId `json:"id"` - } `json:"n_ll"` - } `json:"items"` - } - - err := query.Do(ctx, &queryResponse) - if err != nil { - diags.AddError(fmt.Sprintf("failed to run graph query - %q", query.String()), err.Error()) - return nil - } - - // the query result will include nested (and escaped) JSON. We'll unpack it here. - var ipLinkAttributes struct { - Vlan *int64 `json:"vlan_id"` - } - - // prepare the result map - result := make(map[apstra.ObjectId]map[int64]apstra.ObjectId) - - addToResult := func(ctId apstra.ObjectId, vlan int64, linkId apstra.ObjectId) { - innerMap, ok := result[ctId] - if !ok { - innerMap = make(map[int64]apstra.ObjectId) - } - innerMap[vlan] = linkId - result[ctId] = innerMap - } - _ = addToResult - - for _, item := range queryResponse.Items { - // un-quote the embedded JSON string - attributes, err := strconv.Unquote(string(item.Iplp.Attributes)) - if err != nil { - diags.AddError(fmt.Sprintf("failed to un-quote IP Link attributes - '%s'", item.Iplp.Attributes), err.Error()) - return nil - } - - // unpack the embedded JSON string - err = json.Unmarshal([]byte(attributes), &ipLinkAttributes) - if err != nil { - diags.AddError(fmt.Sprintf("failed to unmarshal IP Link attributes - '%s'", attributes), err.Error()) - return nil - } - - // if the IP Link Primitive matches the Subinterface, collect the result - switch { - case ipLinkAttributes.Vlan == nil && item.Si.Vlan == nil: - // found the matching untagged IP Link and Subinterface - save as VLAN 0 - addToResult(item.Ct.Id, 0, item.Ll.Id) - continue - case ipLinkAttributes.Vlan == nil || item.Si.Vlan == nil: - // one item is untagged, but the other is not - not interesting - continue - case ipLinkAttributes.Vlan != nil && item.Si.Vlan != nil && *ipLinkAttributes.Vlan == *item.Si.Vlan: - // IP link and subinterface are tagged - and they have matching values! - addToResult(item.Ct.Id, *ipLinkAttributes.Vlan, item.Ll.Id) - continue - } - } - - return result -} diff --git a/apstra/utils/ct_ip_link_test.go b/apstra/utils/ct_ip_link_test.go deleted file mode 100644 index 3f97d6cd..00000000 --- a/apstra/utils/ct_ip_link_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package utils - -import ( - "context" - "encoding/json" - "github.com/Juniper/apstra-go-sdk/apstra" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stretchr/testify/require" - "log" - "testing" -) - -func TestThing(t *testing.T) { - ctx := context.Background() - - clientCfg := apstra.ClientCfg{ - Url: "https://apstra-2bd238e9-290d-4f8d-9f3a-05b501becd14.aws.apstra.com", - User: "admin", - Pass: "WillingMockingbird8-", - } - - client, err := clientCfg.NewClient(ctx) - require.NoError(t, err) - - bp, err := client.NewTwoStageL3ClosClient(ctx, "22044be2-e7af-462d-847a-ce6d0b49000e") - require.NoError(t, err) - - var diags diag.Diagnostics - - ctId := "5765435a-acdd-46e7-81dc-a884dc4478ab" - //ctId := "d7d2daa1-4b15-4db6-8982-d4ff64a6a723" - apIds := []string{"IZiK1TY0amaN8UfU9Gc", "UVJpVynfmBMy74Nks4Y"} - - vlanToSubinterfaces := GetCtIpLinkIdsByCtAndAps(ctx, bp, ctId, apIds, &diags) - require.False(t, diags.HasError()) - - m, err := json.MarshalIndent(vlanToSubinterfaces, "", " ") - require.NoError(t, err) - - log.Println(string(m)) -} - -func TestThing2(t *testing.T) { - ctx := context.Background() - - clientCfg := apstra.ClientCfg{ - Url: "https://apstra-2bd238e9-290d-4f8d-9f3a-05b501becd14.aws.apstra.com", - User: "admin", - Pass: "WillingMockingbird8-", - } - - client, err := clientCfg.NewClient(ctx) - require.NoError(t, err) - - bp, err := client.NewTwoStageL3ClosClient(ctx, "22044be2-e7af-462d-847a-ce6d0b49000e") - require.NoError(t, err) - - var diags diag.Diagnostics - - apId := "IZiK1TY0amaN8UfU9Gc" - ctIds := []string{ - //"5765435a-acdd-46e7-81dc-a884dc4478ab", - //"d7d2daa1-4b15-4db6-8982-d4ff64a6a723", - //"2d2cb4f3-9603-4daa-83c1-5c0f142bb6da", - } - - vlanToSubinterfaces := GetCtIpLinkIdsByApAndCts(ctx, bp, apId, ctIds, &diags) - require.False(t, diags.HasError()) - - m, err := json.MarshalIndent(vlanToSubinterfaces, "", " ") - require.NoError(t, err) - - log.Println(string(m)) -}