Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add link_ids read-only attribute to apstra_datacenter_connectivity_template_assignments and apstra_datacenter_connectivity_templates_assignment resources #701

Merged
merged 7 commits into from
Jul 8, 2024
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
Loading