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_template_assignments.go b/apstra/blueprint/connectivity_template_assignments.go index 2ceadb45..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,8 @@ 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"` } func (o ConnectivityTemplateAssignments) ResourceAttributes() map[string]resourceSchema.Attribute { @@ -43,6 +49,21 @@ 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`.\n" + + "**Note:** requires `fetch_iplink_ids = true`", + Computed: true, + ElementType: types.MapType{ElemType: types.StringType}, + }, } } @@ -88,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/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 d3d4747c..af4e2968 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,16 @@ 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) + if resp.Diagnostics.HasError() { + return + } + + // Set state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -171,6 +185,13 @@ func (o *resourceDatacenterConnectivityTemplateAssignments) Update(ctx context.C err.Error()) } + // 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)...) } diff --git a/apstra/utils/ct_ip_link_subinterfaces.go b/apstra/utils/ct_ip_link_subinterfaces.go deleted file mode 100644 index 9db950ad..00000000 --- a/apstra/utils/ct_ip_link_subinterfaces.go +++ /dev/null @@ -1,124 +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 apstra.ObjectId, 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")} - 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 CT primitives - iplQuery := 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}) - - // 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(iplQuery). - Match(siQuery) - - // collect the query response here - var queryResponse struct { - Items []struct { - IpL struct { - Attributes json.RawMessage `json:"attributes"` - } `json:"n_ipl"` - 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.IpL.Attributes)) - if err != nil { - diags.AddError(fmt.Sprintf("failed to \"unquote\" IP Link attributes - '%s'", item.IpL.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 -} diff --git a/apstra/utils/ct_ip_link_subinterfaces_test.go b/apstra/utils/ct_ip_link_subinterfaces_test.go deleted file mode 100644 index 90fb36f1..00000000 --- a/apstra/utils/ct_ip_link_subinterfaces_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package utils - -import ( - "context" - "github.com/Juniper/apstra-go-sdk/apstra" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stretchr/testify/require" - "log" - "testing" -) - -func TestGetCtIpLinkSubinterfaces(t *testing.T) { - ctx := context.Background() - - clientCfg := apstra.ClientCfg{ - Url: "https://apstra-973cfb91-ecfb-4b46-8715-ce1849d7a041.aws.apstra.com", - User: "admin", - Pass: "CoolDuck3%", - } - - client, err := clientCfg.NewClient(ctx) - require.NoError(t, err) - - bp, err := client.NewTwoStageL3ClosClient(ctx, "e38a9808-6bbf-470d-82f1-f19e33d76c98") - require.NoError(t, err) - - var diags diag.Diagnostics - - ctId := apstra.ObjectId("0bdf84ee-8129-4da6-8b35-c93c4febccce") - apId := apstra.ObjectId("k6L8SJevXl663_qqUlY") - - vlanToSubinterfaces := GetCtIpLinkSubinterfaces(ctx, bp, ctId, apId, &diags) - require.False(t, diags.HasError()) - - log.Println(vlanToSubinterfaces) -} 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_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` + 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` +