Skip to content

Commit

Permalink
Merge pull request #262 from Juniper/clear-cts-from-gs-bonds
Browse files Browse the repository at this point in the history
Handle multiple types of "CT attached to link" error
  • Loading branch information
chrismarget-j committed Apr 30, 2024
2 parents 497c16d + 661819c commit e0a57ec
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 16 deletions.
47 changes: 47 additions & 0 deletions apstra/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -660,3 +660,50 @@ func testTemplateB(ctx context.Context, t *testing.T, client *Client) ObjectId {

return id
}

func testSecurityZone(t testing.TB, ctx context.Context, bp *TwoStageL3ClosClient) ObjectId {
t.Helper()

rs := randString(6, "hex")

id, err := bp.CreateSecurityZone(ctx, &SecurityZoneData{
Label: rs,
SzType: SecurityZoneTypeEVPN,
VrfName: rs,
RoutingPolicyId: "",
RouteTarget: nil,
RtPolicy: nil,
VlanId: nil,
VniId: nil,
JunosEvpnIrbMode: nil,
})
require.NoError(t, err)

return id
}

func testVirtualNetwork(t testing.TB, ctx context.Context, bp *TwoStageL3ClosClient, szId ObjectId) ObjectId {
t.Helper()

var vnBindings []VnBinding
nodeMap, err := bp.GetAllSystemNodeInfos(ctx)
require.NoError(t, err)

for _, node := range nodeMap {
if node.Role == SystemRoleLeaf {
vnBindings = append(vnBindings, VnBinding{SystemId: node.Id})
}
}

id, err := bp.CreateVirtualNetwork(ctx, &VirtualNetworkData{
Ipv4Enabled: true,
Label: randString(6, "hex"),
SecurityZoneId: szId,
VirtualGatewayIpv4Enabled: true,
VnBindings: vnBindings,
VnType: VnTypeVxlan,
})
require.NoError(t, err)

return id
}
13 changes: 11 additions & 2 deletions apstra/talk_to_apstra.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ const (
peekSizeForApstraTaskIdResponse = math.MaxUint8

linkHasCtAssignedErrRegexString = "Link with id (.*) can not be deleted since some of its interfaces have connectivity templates assigned"
lagHasCtAssignedErrRegexString = "Deleting all links forming a LAG is not allowed since the LAG has assigned structures: \\[.*'connectivity template'.*]. Link ids: \\[(.*)]"
linkHasVnEndpointErrRegexString = "Link with id (.*) can not be deleted since some of its interfaces have VN endpoints"
)

var (
regexpApiUrlDeleteSwitchSystemLinks = regexp.MustCompile(strings.ReplaceAll(apiUrlDeleteSwitchSystemLinks, "%s", ".*"))
regexpLinkHasCtAssignedErr = regexp.MustCompile(linkHasCtAssignedErrRegexString)
regexpLagHasCtAssignedErr = regexp.MustCompile(lagHasCtAssignedErrRegexString)
regexpLinkHasVnEndpoint = regexp.MustCompile(linkHasVnEndpointErrRegexString)
)

// talkToApstraIn is the input structure for the Client.talkToApstra() function
Expand Down Expand Up @@ -81,8 +85,13 @@ func convertTtaeToAceWherePossible(err error) error {
return ClientErr{errType: ErrCannotChangeTransform, err: errors.New(ttae.Msg)}
case strings.Contains(ttae.Msg, "does not exist"):
return ClientErr{errType: ErrNotfound, err: errors.New(ttae.Msg)}
case regexpLinkHasCtAssignedErr.MatchString(ttae.Msg) && regexpApiUrlDeleteSwitchSystemLinks.MatchString(ttae.Request.URL.Path):
return ClientErr{errType: ErrCtAssignedToLink, err: errors.New(ttae.Msg)}
case regexpApiUrlDeleteSwitchSystemLinks.MatchString(ttae.Request.URL.Path):
switch {
case regexpLinkHasCtAssignedErr.MatchString(ttae.Msg):
return ClientErr{errType: ErrCtAssignedToLink, err: errors.New(ttae.Msg)}
case regexpLagHasCtAssignedErr.MatchString(ttae.Msg):
return ClientErr{errType: ErrCtAssignedToLink, err: errors.New(ttae.Msg)}
}
}
case http.StatusInternalServerError:
switch {
Expand Down
135 changes: 121 additions & 14 deletions apstra/two_stage_l3_clos_switch_system_links.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
)

const (
Expand All @@ -14,8 +15,10 @@ const (
apiUrlBlueprintExternalGenericSystem = apiUrlBlueprintByIdPrefix + "external-generic-systems" + apiUrlPathDelim + "%s"
)

type SystemType int
type systemType string
type (
SystemType int
systemType string
)

const (
SystemTypeExternal = SystemType(iota)
Expand Down Expand Up @@ -178,7 +181,7 @@ type SwitchLinkEndpoint struct {
TransformationId int
SystemId ObjectId
IfName string
//LagMode RackLinkLagMode
// LagMode RackLinkLagMode
}

func (o *SwitchLinkEndpoint) raw() rawSwitchLinkEndpoint {
Expand Down Expand Up @@ -251,25 +254,126 @@ func (o *TwoStageL3ClosClient) DeleteLinksFromSystem(ctx context.Context, ids []
return err // unmarshal fail - surface the original error
}

var linkErrs struct {
// unpack the error
var e struct {
LinkIds []string `json:"link_ids"`
}
if json.Unmarshal(ds.Errors, &linkErrs) != nil {
if json.Unmarshal(ds.Errors, &e) != nil {
return err // unmarshal fail - surface the original error
}

var aceDetail ErrCtAssignedToLinkDetail
for _, linkIdErr := range linkErrs.LinkIds {
matches := regexpLinkHasCtAssignedErr.FindStringSubmatch(linkIdErr)
if len(matches) == 2 {
aceDetail.LinkIds = append(aceDetail.LinkIds, ObjectId(matches[1]))
// we know about two categories of error - use regexes to filter 'em out - examples:
// "Link with id l2_virtual_004_leaf1<->l2_virtual_004_sys003(link-000000002)[1] can not be deleted since some of its interfaces have connectivity templates assigned",
// "Deleting all links forming a LAG is not allowed since the LAG has assigned structures: ['connectivity template', 'VN endpoint']. Link ids: ['l2_virtual_003_leaf1<->l2_virtual_003_sys003(b)[1]', 'l2_virtual_003_leaf1<->l2_virtual_003_sys003(b)[2]']",
var linkErrs []string
var lagErrs []string
for _, le := range e.LinkIds {
switch {
case regexpLinkHasCtAssignedErr.MatchString(le):
linkErrs = append(linkErrs, le)
case regexpLagHasCtAssignedErr.MatchString(le):
lagErrs = append(lagErrs, le)
case regexpLinkHasVnEndpoint.MatchString(le):
// do nothing - this condition should trigger the regexpLinkHasCtAssignedErr also
default: // cannot handle error - surface it to the user
return fmt.Errorf("cannot handle link error %q - %w", le, err)
}
}

ace.detail = aceDetail
// Collect the IDs of links with errors
linkErrCount := len(linkErrs)
lagErrCount := len(lagErrs)
linkIdsWithCts := make([]ObjectId, linkErrCount+lagErrCount)

// extract ids of naked links with errors
for i, s := range linkErrs {
m := regexpLinkHasCtAssignedErr.FindStringSubmatch(s)
if len(m) != 2 {
return fmt.Errorf("cannot handle link error %q - %w", s, err)
}

linkIdsWithCts[i] = ObjectId(m[1])
}

// determine ids of aggregate links with errors
for i, s := range lagErrs {
m := regexpLagHasCtAssignedErr.FindStringSubmatch(s)
if len(m) != 2 {
return fmt.Errorf("cannot handle lag link error %q - %w", s, err)
}

// each lag error enumerates all member links. Extract them
var lagMembers []ObjectId
for _, quotedId := range strings.Split(m[1], ",") {
lagMembers = append(lagMembers, ObjectId(strings.Trim(quotedId, "' ")))
}

// find the LAG ID common to these member IDs
lagId, err := o.lagIdFromMemberIds(ctx, lagMembers)
if err != nil {
return errors.Join(ace, err)
}

linkIdsWithCts[linkErrCount+i] = lagId
}

ace.detail = ErrCtAssignedToLinkDetail{LinkIds: linkIdsWithCts}
return ace
}

func (o *TwoStageL3ClosClient) lagIdFromMemberIds(ctx context.Context, members []ObjectId) (ObjectId, error) {
mq := new(MatchQuery).
SetBlueprintId(o.blueprintId).
SetClient(o.client)

for _, member := range members {
mq.Match(new(PathQuery).
Node([]QEEAttribute{
NodeTypeLink.QEEAttribute(),
{Key: "id", Value: QEStringVal(member.String())},
}).
In([]QEEAttribute{RelationshipTypeLink.QEEAttribute()}).
Node([]QEEAttribute{NodeTypeInterface.QEEAttribute()}).
In([]QEEAttribute{RelationshipTypeComposedOf.QEEAttribute()}).
Node([]QEEAttribute{NodeTypeInterface.QEEAttribute()}).
Out([]QEEAttribute{RelationshipTypeLink.QEEAttribute()}).
Node([]QEEAttribute{
NodeTypeLink.QEEAttribute(),
{Key: "link_type", Value: QEStringVal("aggregate_link")},
{Key: "name", Value: QEStringVal("n_link")},
}),
)
}

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

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

// turn result into a map keyed by link ID - we expect all results to use the same ID (one map entry)
ids := make(map[ObjectId]struct{})
for _, item := range result.Items {
ids[item.Link.Id] = struct{}{}
}

switch len(ids) {
case 0:
return "", fmt.Errorf("member-based LAG member query found no LAG ID - %s", mq.String())
case 1:
return result.Items[0].Link.Id, nil // we expect exactly one map entry (all lag members point at one parent)
default:
return "", fmt.Errorf("member-based LAG member query found more than one LAG ID - %s", mq.String())
}
}

func (o *TwoStageL3ClosClient) DeleteGenericSystem(ctx context.Context, id ObjectId) error {
response := struct {
Items []struct {
Expand All @@ -285,17 +389,20 @@ func (o *TwoStageL3ClosClient) DeleteGenericSystem(ctx context.Context, id Objec
SetBlueprintId(o.blueprintId).
SetBlueprintType(BlueprintTypeStaging).
SetClient(o.client).
Node([]QEEAttribute{NodeTypeSystem.QEEAttribute(),
Node([]QEEAttribute{
NodeTypeSystem.QEEAttribute(),
{Key: "role", Value: QEStringVal("generic")},
{Key: "id", Value: QEStringVal(id)},
{Key: "name", Value: QEStringVal("n_system")},
}).
Out([]QEEAttribute{RelationshipTypeHostedInterfaces.QEEAttribute()}).
Node([]QEEAttribute{NodeTypeInterface.QEEAttribute(),
Node([]QEEAttribute{
NodeTypeInterface.QEEAttribute(),
{Key: "if_type", Value: QEStringVal("ethernet")},
}).
Out([]QEEAttribute{RelationshipTypeLink.QEEAttribute()}).
Node([]QEEAttribute{NodeTypeLink.QEEAttribute(),
Node([]QEEAttribute{
NodeTypeLink.QEEAttribute(),
{Key: "name", Value: QEStringVal("n_link")},
})

Expand Down

0 comments on commit e0a57ec

Please sign in to comment.