Skip to content

Commit

Permalink
Merge pull request #701 from Juniper/ct-application-link-ids
Browse files Browse the repository at this point in the history
Add `link_ids` read-only attribute to `apstra_datacenter_connectivity_template_assignments` and `apstra_datacenter_connectivity_templates_assignment` resources
  • Loading branch information
chrismarget-j authored Jul 8, 2024
2 parents 1a75eb4 + d14541e commit bcbe1cb
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 168 deletions.
1 change: 1 addition & 0 deletions apstra/blueprint/connectivity_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
157 changes: 157 additions & 0 deletions apstra/blueprint/connectivity_template_assignments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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},
},
}
}

Expand Down Expand Up @@ -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...)
}
160 changes: 160 additions & 0 deletions apstra/blueprint/connectivity_templates_assignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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},
},
}
}

Expand All @@ -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...)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
Loading

0 comments on commit bcbe1cb

Please sign in to comment.