diff --git a/apstra/blueprint/device_allocation_system_attributes.go b/apstra/blueprint/device_allocation_system_attributes.go index 74990164..e5df2677 100644 --- a/apstra/blueprint/device_allocation_system_attributes.go +++ b/apstra/blueprint/device_allocation_system_attributes.go @@ -6,13 +6,16 @@ import ( "fmt" "math" "net" + "net/netip" "regexp" "strconv" "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/apstra-go-sdk/apstra/enum" + "github.com/Juniper/terraform-provider-apstra/apstra/compatibility" "github.com/Juniper/terraform-provider-apstra/apstra/constants" "github.com/Juniper/terraform-provider-apstra/apstra/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -204,6 +207,9 @@ func (o *DeviceAllocationSystemAttributes) getLoopback0Addresses(ctx context.Con } idx := 0 + // todo: Fold getLoopbackInterfaceNode into this function, or at least clean up the + // passing of json.RawMessage because we're only using it for a single purpose now. + // Maybe use this: https://github.com/Juniper/apstra-go-sdk/issues/421 rawJson := getLoopbackInterfaceNode(ctx, bp, nodeId, idx, diags) if diags.HasError() { return @@ -364,50 +370,72 @@ func (o *DeviceAllocationSystemAttributes) setAsn(ctx context.Context, bp *apstr } } -func (o *DeviceAllocationSystemAttributes) setLoopbacks(ctx context.Context, bp *apstra.TwoStageL3ClosClient, nodeId apstra.ObjectId, diags *diag.Diagnostics) { - if !utils.HasValue(o.LoopbackIpv4) && !utils.HasValue(o.LoopbackIpv6) { - return - } +func getLoopbackNodeAndSecurityZoneIDs(ctx context.Context, bp *apstra.TwoStageL3ClosClient, systemNodeId apstra.ObjectId, loopIdx int, diags *diag.Diagnostics) (apstra.ObjectId, apstra.ObjectId) { + query := new(apstra.PathQuery). + SetBlueprintId(bp.Id()). + SetClient(bp.Client()). + Node([]apstra.QEEAttribute{ + apstra.NodeTypeSystem.QEEAttribute(), + {Key: "id", Value: apstra.QEStringVal(systemNodeId)}, + }). + Out([]apstra.QEEAttribute{apstra.RelationshipTypeHostedInterfaces.QEEAttribute()}). + Node([]apstra.QEEAttribute{ + apstra.NodeTypeInterface.QEEAttribute(), + {Key: "if_type", Value: apstra.QEStringVal("loopback")}, + {Key: "loopback_id", Value: apstra.QEIntVal(loopIdx)}, + {Key: "name", Value: apstra.QEStringVal("n_interface")}, + }). + In([]apstra.QEEAttribute{apstra.RelationshipTypeMemberInterfaces.QEEAttribute()}). + Node([]apstra.QEEAttribute{apstra.NodeTypeSecurityZoneInstance.QEEAttribute()}). + In([]apstra.QEEAttribute{apstra.RelationshipTypeInstantiatedBy.QEEAttribute()}). + Node([]apstra.QEEAttribute{ + apstra.NodeTypeSecurityZone.QEEAttribute(), + {Key: "name", Value: apstra.QEStringVal("n_security_zone")}, + }) - idx := 0 - rawJson := getLoopbackInterfaceNode(ctx, bp, nodeId, idx, diags) - if diags.HasError() { - return + var queryResponse struct { + Items []struct { + Interface struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_interface"` + SecurityZone struct { + Id apstra.ObjectId `json:"id"` + } `json:"n_security_zone"` + } `json:"items"` } - if len(rawJson) == 0 && utils.HasValue(o.LoopbackIpv4) { - diags.AddAttributeError( - path.Root("system_attributes").AtName("loopback_ipv4"), - "Cannot set loopback address", - fmt.Sprintf("system %q has no associated loopback %d node -- is it a spine or leaf switch?", nodeId, idx)) - } - if len(rawJson) == 0 && utils.HasValue(o.LoopbackIpv6) { - diags.AddAttributeError( - path.Root("system_attributes").AtName("loopback_ipv6"), - "Cannot set loopback address", - fmt.Sprintf("system %q has no associated loopback %d node -- is it a spine or leaf switch?", nodeId, idx)) + err := query.Do(ctx, &queryResponse) + if err != nil { + diags.AddError("failed while querying for loopback interface and security zone", err.Error()) + return "", "" } - if diags.HasError() { - return + if len(queryResponse.Items) != 1 { + diags.AddError( + fmt.Sprintf("expected exactly one loopback and security zone node pair, got %d", len(queryResponse.Items)), + fmt.Sprintf("graph query: %q", query), + ) + return "", "" } - var loopbackNode struct { - Id *apstra.ObjectId `json:"id"` - } + ifId, szId := queryResponse.Items[0].Interface.Id, queryResponse.Items[0].SecurityZone.Id - err := json.Unmarshal(rawJson, &loopbackNode) - if err != nil { - diags.AddError(fmt.Sprintf("failed while unpacking system %q loopback %d node", nodeId, idx), err.Error()) - return + if ifId == "" { + diags.AddError( + "got empty interface ID", + fmt.Sprintf("graph query: %q", query), + ) } - - if loopbackNode.Id == nil { + if szId == "" { diags.AddError( - fmt.Sprintf("failed parsing loopback %d node linked with node %q", idx, nodeId), - fmt.Sprintf("loopback %d node has no field `id`: %q", idx, string(rawJson))) - return + "got empty security zone ID", + fmt.Sprintf("graph query: %q", query), + ) } + return ifId, szId +} + +func (o *DeviceAllocationSystemAttributes) legacySetLoopbacks(ctx context.Context, bp *apstra.TwoStageL3ClosClient, nodeId apstra.ObjectId, diags *diag.Diagnostics) { patch := &struct { IPv4Addr string `json:"ipv4_addr,omitempty"` IPv6Addr string `json:"ipv6_addr,omitempty"` @@ -416,9 +444,48 @@ func (o *DeviceAllocationSystemAttributes) setLoopbacks(ctx context.Context, bp IPv6Addr: o.LoopbackIpv6.ValueString(), } - err = bp.PatchNode(ctx, *loopbackNode.Id, &patch, nil) + err := bp.PatchNode(ctx, nodeId, &patch, nil) + if err != nil { + diags.AddError(fmt.Sprintf("failed setting loopback addresses to interface node %q", nodeId), err.Error()) + return + } +} + +func (o *DeviceAllocationSystemAttributes) setLoopbacks(ctx context.Context, bp *apstra.TwoStageL3ClosClient, nodeId apstra.ObjectId, diags *diag.Diagnostics) { + if !utils.HasValue(o.LoopbackIpv4) && !utils.HasValue(o.LoopbackIpv6) { + return + } + + idx := 0 // we always are interested in loopback 0 + + loopbackNodeId, securityZoneId := getLoopbackNodeAndSecurityZoneIDs(ctx, bp, nodeId, idx, diags) + if diags.HasError() { + return + } + + if compatibility.ApiNotSupportsSetLoopbackIps.Check(version.Must(version.NewVersion(bp.Client().ApiVersion()))) { + // we must be talking to Apstra 4.x + o.legacySetLoopbacks(ctx, bp, loopbackNodeId, diags) + return + } + + // Use new() here to ensure we have invalid non-nil prefix pointers. These will remove values from the API. + ipv4Addr, ipv6Addr := new(netip.Prefix), new(netip.Prefix) + if utils.HasValue(o.LoopbackIpv4) { + ipv4Addr = utils.ToPtr(netip.MustParsePrefix(o.LoopbackIpv4.ValueString())) + } + if utils.HasValue(o.LoopbackIpv6) { + ipv6Addr = utils.ToPtr(netip.MustParsePrefix(o.LoopbackIpv6.ValueString())) + } + + err := bp.SetSecurityZoneLoopbacks(ctx, securityZoneId, map[apstra.ObjectId]apstra.SecurityZoneLoopback{ + loopbackNodeId: { + IPv4Addr: ipv4Addr, + IPv6Addr: ipv6Addr, + }, + }) if err != nil { - diags.AddError(fmt.Sprintf("failed setting loopback addresses to interface node %q", loopbackNode.Id), err.Error()) + diags.AddError("failed while setting loopback addresses", err.Error()) return } } diff --git a/apstra/compatibility/constraints.go b/apstra/compatibility/constraints.go index 8c21e650..6874a605 100644 --- a/apstra/compatibility/constraints.go +++ b/apstra/compatibility/constraints.go @@ -6,6 +6,7 @@ import ( ) var ( + ApiNotSupportsSetLoopbackIps = versionconstraints.New(apiversions.LtApstra500) BpIbaDashboardOk = versionconstraints.New(apiversions.LtApstra500) BpIbaProbeOk = versionconstraints.New(apiversions.LtApstra500) BpIbaWidgetOk = versionconstraints.New(apiversions.LtApstra500) diff --git a/go.mod b/go.mod index ac455c3f..c888dd8c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.22.5 require ( github.com/IBM/netaddr v1.5.0 - github.com/Juniper/apstra-go-sdk v0.0.0-20240920145043-b30ce0dd776c + github.com/Juniper/apstra-go-sdk v0.0.0-20241001222148-3138d56746ba github.com/chrismarget-j/go-licenses v0.0.0-20240224210557-f22f3e06d3d4 github.com/chrismarget-j/version-constraints v0.0.0-20240925155624-26771a0a6820 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index 160fa7c6..616635f8 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/IBM/netaddr v1.5.0 h1:IJlFZe1+nFs09TeMB/HOP4+xBnX2iM/xgiDOgZgTJq0= github.com/IBM/netaddr v1.5.0/go.mod h1:DDBPeYgbFzoXHjSz9Jwk7K8wmWV4+a/Kv0LqRnb8we4= -github.com/Juniper/apstra-go-sdk v0.0.0-20240920145043-b30ce0dd776c h1:TbwhSEMYKjH2un1G9tpiv8tsK9M21YPkOZsajAdn9R0= -github.com/Juniper/apstra-go-sdk v0.0.0-20240920145043-b30ce0dd776c/go.mod h1:qXNVTdnVa40aMTOsBTnKoFNYT5ftga2NAkGJhx4o6bY= +github.com/Juniper/apstra-go-sdk v0.0.0-20241001222148-3138d56746ba h1:ceuHpbkoiAKy3CI7pgZk5q6EmYTljFoLBybTOl3pH/A= +github.com/Juniper/apstra-go-sdk v0.0.0-20241001222148-3138d56746ba/go.mod h1:qXNVTdnVa40aMTOsBTnKoFNYT5ftga2NAkGJhx4o6bY= github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0= github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=