Skip to content

Commit

Permalink
Merge pull request #658 from Juniper/bug/657-improve-clear-cts-on-des…
Browse files Browse the repository at this point in the history
…troy

Extend generic system `clear_cts_on_destroy` behavior
  • Loading branch information
chrismarget-j committed May 6, 2024
2 parents bac49b0 + d382d80 commit fd592ee
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 22 deletions.
192 changes: 172 additions & 20 deletions apstra/blueprint/datacenter_generic_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"regexp"
"sort"

"github.com/Juniper/terraform-provider-apstra/apstra/constants"

"github.com/Juniper/apstra-go-sdk/apstra"
apiversions "github.com/Juniper/terraform-provider-apstra/apstra/api_versions"
"github.com/Juniper/terraform-provider-apstra/apstra/design"
Expand Down Expand Up @@ -160,9 +162,13 @@ func (o DatacenterGenericSystem) ResourceAttributes() map[string]resourceSchema.
Validators: []validator.String{stringvalidator.OneOf(utils.AllNodeDeployModes()...)},
},
"clear_cts_on_destroy": resourceSchema.BoolAttribute{
MarkdownDescription: "When `true`, Link deletion in `destroy` phase and `apply` phase (where a Link has " +
"been removed from the configuration) will automatically clear Connectivity Template assignments " +
"from interfaces associated with those Links.",
MarkdownDescription: "When `true`, Connectivity Templates associated with this Generic System will be " +
"automatically cleared in a variety of circumstances where they would ordinarily block Generic System " +
"changes, including:\n" +
" - Deletion of the Generic System\n" +
" - Deletion of a Generic System Link or LAG interface\n" +
" - Orphaning a LAG interface by reassigning all of its member links to new roles by changing their " +
"`group_label` attribute\n",
Optional: true,
},
}
Expand Down Expand Up @@ -521,29 +527,49 @@ func (o *DatacenterGenericSystem) deleteLinksFromSystem(ctx context.Context, lin
return
}

var ace apstra.ClientErr
var pendingDiags diag.Diagnostics

// try to delete the links
err := bp.DeleteLinksFromSystem(ctx, linkIdsToDelete)
if err == nil {
return // success!
} else {
pendingDiags.AddError(
fmt.Sprintf("failed deleting links %v from generic system %s", linkIdsToDelete, o.Id),
err.Error())
if !(errors.As(err, &ace) && ace.Type() == apstra.ErrCtAssignedToLink && ace.Detail() != nil && o.ClearCtsOnDestroy.ValueBool()) {
// we cannot handle the error
diags.Append(pendingDiags...)
return
}
}

// we got here because some links have CTs attached.
// see if we can handle this error...
var ace apstra.ClientErr
if !errors.As(err, &ace) || ace.Type() != apstra.ErrCtAssignedToLink || ace.Detail() == nil {
// cannot handle error
diags.AddError("failed while deleting Links from Generic System", err.Error())
return
}

// the error detail has to be the correct type...
detail, ok := ace.Detail().(apstra.ErrCtAssignedToLinkDetail)
if !ok {
diags.AddError(
constants.ErrProviderBug+fmt.Sprintf(" - ErrCtAssignedToLink has unexpected detail type: %T", detail),
err.Error(),
)
return
}

// see if the user could have avoided this problem...
if !o.ClearCtsOnDestroy.ValueBool() {
diags.AddWarning(
fmt.Sprintf("Cannot delete links with Connectivity Templates assigned: %v", detail.LinkIds),
"You can set 'clear_cts_on_destroy = true' to override this behavior",
)
return
}

// prep an error diagnostic in case we can't figure this out
var pendingDiags diag.Diagnostics
pendingDiags.AddError(
fmt.Sprintf("failed deleting links %v from generic system %s", linkIdsToDelete, o.Id),
err.Error())

// try to clear the connectivity templates from the problem links
o.ClearConnectivityTemplatesFromLinks(ctx, ace.Detail().(apstra.ErrCtAssignedToLinkDetail).LinkIds, bp, diags)
if diags.HasError() {
diags.Append(pendingDiags...)
diags.Append(pendingDiags...) // throw the pending diagnostic on the pile and give up
return
}

Expand All @@ -552,7 +578,7 @@ func (o *DatacenterGenericSystem) deleteLinksFromSystem(ctx context.Context, lin
if err != nil {
diags.AddError("failed second attempt to delete links after attempting to handle the link deletion error",
err.Error())
diags.Append(pendingDiags...)
diags.Append(pendingDiags...) // throw the pending diagnostic on the pile and give up
return
}
}
Expand Down Expand Up @@ -582,16 +608,142 @@ func (o *DatacenterGenericSystem) updateLinkParams(ctx context.Context, planLink
if len(request) != 0 {
err := bp.SetLinkLagParams(ctx, &request)
if err != nil {
diags.AddError("failed updating generic system link parameters", err.Error()) // collect all errors
// we may be able to figure this out...
var pendingDiags diag.Diagnostics
pendingDiags.AddError("failed updating generic system link parameters", err.Error())

var ace apstra.ClientErr
if !errors.As(err, &ace) || ace.Type() != apstra.ErrLagHasAssignedStructrues || ace.Detail() == nil {
diags.Append(pendingDiags...) // cannot handle error
return
}

detail, ok := ace.Detail().(apstra.ErrLagHasAssignedStructuresDetail)
if !ok || len(detail.GroupLabels) == 0 {
diags.Append(pendingDiags...) // cannot handle error
return
}

var lagIds []apstra.ObjectId
for _, groupLabel := range detail.GroupLabels {
lagId, err := lagLinkIdFromGsIdAndGroupLabel(ctx, bp, apstra.ObjectId(o.Id.ValueString()), groupLabel)
if err != nil {
// return both errors
diags.Append(pendingDiags...)
diags.AddError("failed to determine upstream switch LAG port ID", err.Error())
continue
}

lagIds = append(lagIds, lagId)
}

if !o.ClearCtsOnDestroy.ValueBool() {
diags.Append(pendingDiags...) // cannot handle error
diags.AddWarning(
fmt.Sprintf("Cannot orphan LAGs with Connectivity Templates assigned: %v", lagIds),
"You can set 'clear_cts_on_destroy = true' to override this behavior",
)
return
}

o.ClearConnectivityTemplatesFromLinks(ctx, lagIds, bp, diags)

// try again...
err = bp.SetLinkLagParams(ctx, &request)
if err != nil {
diags.AddError("failed updating generic system LAG parameters after clearing CTs", err.Error()) // cannot handle error
return
}
}
}

// one at a time, check/update each link transform ID
for i, link := range planLinks {
link.updateTransformId(ctx, stateLinks[i], bp, diags) // collect all errors
link.updateTransformId(ctx, stateLinks[i], bp, diags)
}
}

func lagLinkIdFromGsIdAndGroupLabel(ctx context.Context, bp *apstra.TwoStageL3ClosClient, gsId apstra.ObjectId, groupLabel string) (apstra.ObjectId, error) {
query := new(apstra.PathQuery).SetBlueprintId(bp.Id()).SetClient(bp.Client()).
Node([]apstra.QEEAttribute{{Key: "id", Value: apstra.QEStringVal(gsId.String())}}).
Out([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}).
Node([]apstra.QEEAttribute{
apstra.NodeTypeInterface.QEEAttribute(),
{Key: "if_type", Value: apstra.QEStringVal("port_channel")},
}).
Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}).
Node([]apstra.QEEAttribute{
apstra.NodeTypeLink.QEEAttribute(),
{Key: "group_label", Value: apstra.QEStringVal(groupLabel)},
{Key: "link_type", Value: apstra.QEStringVal("aggregate_link")},
{Key: "name", Value: apstra.QEStringVal("n_link")},
})

var result struct {
Items []struct {
Link struct {
Id apstra.ObjectId `json:"id"`
} `json:"n_link"`
} `json:"items"`
}

if err := query.Do(ctx, &result); err != nil {
return "", err
}

switch len(result.Items) {
case 0:
return "", fmt.Errorf("query failed to find LAG link ID for system %q group label %q - %s", gsId, groupLabel, query.String())
case 1:
return result.Items[0].Link.Id, nil
default:
return "", fmt.Errorf("query found multiple find LAG link IDs for system %q group label %q - %s", gsId, groupLabel, query.String())
}
}

//func switchLagIdFromGsIdAndGroupLabel(ctx context.Context, bp *apstra.TwoStageL3ClosClient, gsId apstra.ObjectId, groupLabel string) (apstra.ObjectId, error) {
// query := new(apstra.PathQuery).SetBlueprintId(bp.Id()).SetClient(bp.Client()).
// Node([]apstra.QEEAttribute{{Key: "id", Value: apstra.QEStringVal(gsId.String())}}).
// Out([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}).
// Node([]apstra.QEEAttribute{
// apstra.NodeTypeInterface.QEEAttribute(),
// {Key: "if_type", Value: apstra.QEStringVal("port_channel")},
// }).
// Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}).
// Node([]apstra.QEEAttribute{
// apstra.NodeTypeLink.QEEAttribute(),
// {Key: "group_label", Value: apstra.QEStringVal(groupLabel)},
// {Key: "link_type", Value: apstra.QEStringVal("aggregate_link")},
// }).
// In([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}).
// Node([]apstra.QEEAttribute{
// apstra.NodeTypeInterface.QEEAttribute(),
// {Key: "if_type", Value: apstra.QEStringVal("port_channel")},
// {Key: "name", Value: apstra.QEStringVal("n_application_point")},
// })
//
// var result struct {
// Items []struct {
// ApplicationPoint struct {
// Id apstra.ObjectId `json:"id"`
// } `json:"n_application_point"`
// } `json:"items"`
// }
//
// if err := query.Do(ctx, &result); err != nil {
// return "", err
// }
//
// switch len(result.Items) {
// case 0:
// return "", fmt.Errorf("query failed to find upstream interface ID for system %q group label %q - %s", gsId, groupLabel, query.String())
// case 1:
// return result.Items[0].ApplicationPoint.Id, nil
// default:
// return "", fmt.Errorf("query found multiple find upstream interface IDs for system %q group label %q - %s", gsId, groupLabel, query.String())
// }
//}

// linkIds performs the graph queries necessary to return the link IDs which
// connect this Generic System (o) to the systems+interfaces specified by links.
func (o *DatacenterGenericSystem) linkIds(ctx context.Context, links []*DatacenterGenericSystemLink, bp *apstra.TwoStageL3ClosClient, diags *diag.Diagnostics) []apstra.ObjectId {
Expand Down
89 changes: 89 additions & 0 deletions apstra/resource_datacenter_generic_system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,36 @@ func TestResourceDatacenterGenericSystem_A(t *testing.T) {
require.NoError(t, err)
}

attachCtToLag := func(groupLabel string) {
query := new(apstra.PathQuery).
SetBlueprintId(bpClient.Id()).
SetClient(bpClient.Client()).
Node([]apstra.QEEAttribute{{Key: "id", Value: apstra.QEStringVal(leafIds[0])}}).
Out([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}).
Node([]apstra.QEEAttribute{
apstra.NodeTypeInterface.QEEAttribute(),
{Key: "name", Value: apstra.QEStringVal("n_interface")},
}).
Out([]apstra.QEEAttribute{apstra.RelationshipTypeLink.QEEAttribute()}).
Node([]apstra.QEEAttribute{
apstra.NodeTypeLink.QEEAttribute(),
{Key: "group_label", Value: apstra.QEStringVal(groupLabel)},
{Key: "link_type", Value: apstra.QEStringVal("aggregate_link")},
})
var response struct {
Items []struct {
Interface struct {
Id apstra.ObjectId `json:"id"`
} `json:"n_interface"`
} `json:"items"`
}
err := query.Do(context.Background(), &response)
require.NoError(t, err)

err = bpClient.SetApplicationPointConnectivityTemplates(context.Background(), response.Items[0].Interface.Id, []apstra.ObjectId{*ct.Id})
require.NoError(t, err)
}

type testStep struct {
genericSystem genericSystem
testCheckFunc resource.TestCheckFunc
Expand Down Expand Up @@ -682,6 +712,65 @@ func TestResourceDatacenterGenericSystem_A(t *testing.T) {
},
},
},
"orphan_lag_with_attached_ct": {
steps: []testStep{
{
genericSystem: genericSystem{
clearCtsOnDestroy: true,
links: []link{
{
targetSwitchId: leafIds[0],
targetSwitchIf: "xe-0/0/11",
targetSwitchTf: 1,
},
{
targetSwitchId: leafIds[0],
targetSwitchIf: "xe-0/0/12",
targetSwitchTf: 1,
groupLabel: "foo",
lagMode: apstra.RackLinkLagModeActive,
},
{
targetSwitchId: leafIds[0],
targetSwitchIf: "xe-0/0/13",
targetSwitchTf: 1,
groupLabel: "bar",
lagMode: apstra.RackLinkLagModeActive,
},
},
},
},
{
preConfig: func() {
attachCtToLag("bar")
},
genericSystem: genericSystem{
clearCtsOnDestroy: true,
links: []link{
{
targetSwitchId: leafIds[0],
targetSwitchIf: "xe-0/0/11",
targetSwitchTf: 1,
},
{
targetSwitchId: leafIds[0],
targetSwitchIf: "xe-0/0/12",
targetSwitchTf: 1,
groupLabel: "foo",
lagMode: apstra.RackLinkLagModeActive,
},
{
targetSwitchId: leafIds[0],
targetSwitchIf: "xe-0/0/13",
targetSwitchTf: 1,
groupLabel: "foo",
lagMode: apstra.RackLinkLagModeActive,
},
},
},
},
},
},
}

for tName, tCase := range testCases {
Expand Down
5 changes: 4 additions & 1 deletion docs/resources/datacenter_generic_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ resource "apstra_datacenter_generic_system" "example" {
### Optional

- `asn` (Number) AS number of the Generic System. Note that in some circumstances Apstra may assign an ASN to the generic system even when none is supplied via this attribute. The automaticallyassigned value will be overwritten by Terraform during a subsequent apply operation.
- `clear_cts_on_destroy` (Boolean) When `true`, Link deletion in `destroy` phase and `apply` phase (where a Link has been removed from the configuration) will automatically clear Connectivity Template assignments from interfaces associated with those Links.
- `clear_cts_on_destroy` (Boolean) When `true`, Connectivity Templates associated with this Generic System will be automatically cleared in a variety of circumstances where they would ordinarily block Generic System changes, including:
- Deletion of the Generic System
- Deletion of a Generic System Link or LAG interface
- Orphaning a LAG interface by reassigning all of its member links to new roles by changing their `group_label` attribute
- `deploy_mode` (String) Set the Apstra Deploy Mode for this Generic System. Default: `deploy`
- `external` (Boolean) Set `true` to create an External Generic System
- `hostname` (String) System hostname.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/Juniper/terraform-provider-apstra

go 1.21

// replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk
//replace github.com/Juniper/apstra-go-sdk => ../apstra-go-sdk

toolchain go1.21.1

Expand Down

0 comments on commit fd592ee

Please sign in to comment.