From 2b858108076b45144789741a0a1843bc8333a369 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 17 Oct 2024 21:51:01 -0400 Subject: [PATCH 1/3] introduce `apstra_blueprint_device_rendered_config` data source --- apstra/blueprint/device_rendered_config.go | 57 ++++ ...source_blueprint_device_rendered_config.go | 124 +++++++++ ...device_rendered_config_integration_test.go | 245 ++++++++++++++++++ apstra/export_test.go | 17 ++ apstra/provider.go | 1 + apstra/test_helpers_test.go | 4 +- apstra/test_utils/blueprint.go | 91 +++++++ apstra/test_utils/test_utils.go | 34 +++ go.mod | 2 +- go.sum | 4 +- 10 files changed, 574 insertions(+), 5 deletions(-) create mode 100644 apstra/blueprint/device_rendered_config.go create mode 100644 apstra/data_source_blueprint_device_rendered_config.go create mode 100644 apstra/data_source_blueprint_device_rendered_config_integration_test.go diff --git a/apstra/blueprint/device_rendered_config.go b/apstra/blueprint/device_rendered_config.go new file mode 100644 index 00000000..841a46de --- /dev/null +++ b/apstra/blueprint/device_rendered_config.go @@ -0,0 +1,57 @@ +package blueprint + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type RenderedConfig struct { + BlueprintId types.String `tfsdk:"blueprint_id"` + SystemId types.String `tfsdk:"system_id"` + NodeId types.String `tfsdk:"node_id"` + StagedCfg types.String `tfsdk:"staged_config""` + DeployedCfg types.String `tfsdk:"deployed_config""` + Incremental types.String `tfsdk:"incremental_config"` +} + +func (o RenderedConfig) DataSourceAttributes() map[string]dataSourceSchema.Attribute { + return map[string]dataSourceSchema.Attribute{ + "blueprint_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra Blueprint ID.", + Required: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "system_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra ID (serial number) for the System (Managed Device), as found in " + + "Devices -> Managed Devices in the GUI. Required when `node_id` is omitted.", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.ExactlyOneOf( + path.MatchRoot("system_id"), + path.MatchRoot("node_id"), + ), + }, + }, + "node_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Apstra ID of the System (spine, leaf, etc...) node. Required when `system_id` is omitted.", + Optional: true, + Validators: []validator.String{stringvalidator.LengthAtLeast(1)}, + }, + "staged_config": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Staged device configuration.", + Computed: true, + }, + "deployed_config": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Deployed device configuration.", + Computed: true, + }, + "incremental_config": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Incremental device configuration.", + Computed: true, + }, + } +} diff --git a/apstra/data_source_blueprint_device_rendered_config.go b/apstra/data_source_blueprint_device_rendered_config.go new file mode 100644 index 00000000..f64b2051 --- /dev/null +++ b/apstra/data_source_blueprint_device_rendered_config.go @@ -0,0 +1,124 @@ +package tfapstra + +import ( + "context" + "errors" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + "github.com/Juniper/apstra-go-sdk/apstra/enum" + "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintNodeConfig{} +var _ datasourceWithSetClient = &dataSourceBlueprintNodeConfig{} + +type dataSourceBlueprintNodeConfig struct { + client *apstra.Client +} + +func (o *dataSourceBlueprintNodeConfig) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_blueprint_device_rendered_config" +} + +func (o *dataSourceBlueprintNodeConfig) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + configureDataSource(ctx, o, req, resp) +} + +func (o *dataSourceBlueprintNodeConfig) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: docCategoryRefDesignAny + + "This data source retrieves rendered device configuration for a system in a Blueprint.", + Attributes: blueprint.RenderedConfig{}.DataSourceAttributes(), + } +} + +func (o *dataSourceBlueprintNodeConfig) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Retrieve values from config. + var config blueprint.RenderedConfig + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + addNodeDiag := func(err error) { + var ace apstra.ClientErr + if errors.As(err, &ace) && ace.Type() == apstra.ErrNotfound { + resp.Diagnostics.AddError( + "Not Found", + fmt.Sprintf("Node %s in blueprint %s not found", config.NodeId, config.BlueprintId), + ) + } else { + resp.Diagnostics.AddError("Failed to fetch config", err.Error()) + } + } + + addSysDiag := func(err error) { + var ace apstra.ClientErr + if errors.As(err, &ace) && ace.Type() == apstra.ErrNotfound { + resp.Diagnostics.AddError( + "Not Found", + fmt.Sprintf("System %s in blueprint %s not found", config.SystemId, config.BlueprintId), + ) + } else { + resp.Diagnostics.AddError("Failed to fetch config", err.Error()) + } + } + + bpId := apstra.ObjectId(config.BlueprintId.ValueString()) + + var err error + var deployed, staged, incremental string + + switch { + case !config.NodeId.IsNull(): + node := apstra.ObjectId(config.NodeId.ValueString()) + deployed, err = o.client.GetNodeRenderedConfig(ctx, bpId, node, enum.RenderedConfigTypeDeployed) + if err != nil { + addNodeDiag(err) + return + } + staged, err = o.client.GetNodeRenderedConfig(ctx, bpId, node, enum.RenderedConfigTypeStaging) + if err != nil { + addNodeDiag(err) + return + } + diff, err := o.client.GetNodeRenderedConfigDiff(ctx, bpId, node) + if err != nil { + addNodeDiag(err) + return + } + incremental = diff.Config + case !config.SystemId.IsNull(): + system := apstra.ObjectId(config.SystemId.ValueString()) + deployed, err = o.client.GetSystemRenderedConfig(ctx, bpId, system, enum.RenderedConfigTypeDeployed) + if err != nil { + addSysDiag(err) + return + } + staged, err = o.client.GetSystemRenderedConfig(ctx, bpId, system, enum.RenderedConfigTypeStaging) + if err != nil { + addSysDiag(err) + return + } + diff, err := o.client.GetSystemRenderedConfigDiff(ctx, bpId, system) + if err != nil { + addSysDiag(err) + return + } + incremental = diff.Config + } + + config.DeployedCfg = types.StringValue(deployed) + config.StagedCfg = types.StringValue(staged) + config.Incremental = types.StringValue(incremental) + + // set state + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (o *dataSourceBlueprintNodeConfig) setClient(client *apstra.Client) { + o.client = client +} diff --git a/apstra/data_source_blueprint_device_rendered_config_integration_test.go b/apstra/data_source_blueprint_device_rendered_config_integration_test.go new file mode 100644 index 00000000..01abb622 --- /dev/null +++ b/apstra/data_source_blueprint_device_rendered_config_integration_test.go @@ -0,0 +1,245 @@ +//go:build integration + +package tfapstra_test + +import ( + "bufio" + "context" + "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" + tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" + testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" + "math" + "math/rand/v2" + "strconv" + "strings" + "sync" + "testing" +) + +const dataSourceBlueprintDeviceRenderedConfigHCL = ` +data %q "test" { + blueprint_id = %q + node_id = %s + system_id = %s +} +` + +type dataSourceBlueprintDeviceRenderedConfig struct { + nodeId apstra.ObjectId + systemId apstra.ObjectId +} + +func (o dataSourceBlueprintDeviceRenderedConfig) render(rType, bpId string) string { + return fmt.Sprintf(dataSourceBlueprintDeviceRenderedConfigHCL, + rType, + bpId, + stringOrNull(o.nodeId), + stringOrNull(o.systemId), + ) +} + +func TestAccDatasourceBlueprintDeviceRenderedConfig(t *testing.T) { + ctx := context.Background() + bp := testutils.BlueprintI(t, ctx) + leafMap := testutils.GetSystemIds(t, ctx, bp, "leaf") + require.Equal(t, 2, len(leafMap)) + + type testCase struct { + preFunc func(testing.TB, context.Context, *apstra.TwoStageL3ClosClient) + config dataSourceBlueprintDeviceRenderedConfig + checks []resource.TestCheckFunc + } + + nodeLabels := make([]string, len(leafMap)) + nodeIds := make([]apstra.ObjectId, len(leafMap)) + sysIds := make([]apstra.ObjectId, len(leafMap)) + var i int + for k, v := range leafMap { + var node struct { + SystemId apstra.ObjectId `json:"system_id"` + } + require.NoError(t, bp.Client().GetNode(ctx, bp.Id(), v, &node)) + + nodeLabels[i] = k + nodeIds[i] = v + sysIds[i] = node.SystemId + + i++ + } + + changeLeafAsn := func(t testing.TB, ctx context.Context, leafId apstra.ObjectId, client *apstra.TwoStageL3ClosClient) { + t.Helper() + + query := new(apstra.PathQuery). + SetBlueprintId(bp.Id()). + SetClient(bp.Client()). + Node([]apstra.QEEAttribute{ + apstra.NodeTypeSystem.QEEAttribute(), + {Key: "id", Value: apstra.QEStringVal(leafId)}, + }). + In([]apstra.QEEAttribute{apstra.RelationshipTypeComposedOfSystems.QEEAttribute()}). + Node([]apstra.QEEAttribute{ + apstra.NodeTypeDomain.QEEAttribute(), + {Key: "name", Value: apstra.QEStringVal("n_domain")}, + }) + + type node struct { + Id apstra.ObjectId `json:"id"` + Asn string `json:"domain_id"` + } + + var queryResult struct { + Items []struct { + Node node `json:"n_domain"` + } `json:"items"` + } + + err := query.Do(ctx, &queryResult) + require.NoError(t, err) + require.Equal(t, 1, len(queryResult.Items)) + + err = client.Client().PatchNodeUnsafe(ctx, bp.Id(), queryResult.Items[0].Node.Id, node{Asn: strconv.Itoa(rand.IntN(math.MaxUint16))}, nil) + require.NoError(t, err) + } + + datasourceType := tfapstra.DatasourceName(ctx, &tfapstra.DataSourceBlueprintNodeConfig) + + atLeast100Lines := func(value string) error { + s := bufio.NewScanner(strings.NewReader(value)) + var i int + for s.Scan() { + i++ + if i >= 100 { + return nil + } + } + return fmt.Errorf("expected 100 lines, got %d lines", i) + } + + expectAsnChange := func(value string) error { + var asnAdded, asnRemoved bool + s := bufio.NewScanner(strings.NewReader(value)) + for s.Scan() { + line := s.Text() + switch { + case strings.HasPrefix(line, "- autonomous-system "): + asnRemoved = true + case strings.HasPrefix(line, "+ autonomous-system "): + asnAdded = true + } + } + if !asnAdded || !asnRemoved { + return fmt.Errorf("diff should show ASN removed and added; got %q", value) + } + return nil + } + + testCases := map[string]testCase{ + "leaf_0_pre_change_by_node": { + config: dataSourceBlueprintDeviceRenderedConfig{nodeId: nodeIds[0]}, + checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("data."+datasourceType+".test", "blueprint_id", bp.Id().String()), + resource.TestCheckResourceAttr("data."+datasourceType+".test", "node_id", nodeIds[0].String()), + resource.TestCheckNoResourceAttr("data."+datasourceType+".test", "system_id"), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "deployed_config"), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "staged_config"), + resource.TestCheckResourceAttr("data."+datasourceType+".test", "incremental_config", ""), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "deployed_config", atLeast100Lines), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "staged_config", atLeast100Lines), + }, + }, + "leaf_0_pre_change_by_system": { + config: dataSourceBlueprintDeviceRenderedConfig{systemId: sysIds[0]}, + checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("data."+datasourceType+".test", "blueprint_id", bp.Id().String()), + resource.TestCheckNoResourceAttr("data."+datasourceType+".test", "node_id"), + resource.TestCheckResourceAttr("data."+datasourceType+".test", "system_id", sysIds[0].String()), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "deployed_config"), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "staged_config"), + resource.TestCheckResourceAttr("data."+datasourceType+".test", "incremental_config", ""), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "deployed_config", atLeast100Lines), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "staged_config", atLeast100Lines), + }, + }, + "leaf_0_post_change_by_node": { + preFunc: func(t testing.TB, ctx context.Context, client *apstra.TwoStageL3ClosClient) { + changeLeafAsn(t, ctx, nodeIds[0], bp) + }, + config: dataSourceBlueprintDeviceRenderedConfig{nodeId: nodeIds[0]}, + checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("data."+datasourceType+".test", "blueprint_id", bp.Id().String()), + resource.TestCheckResourceAttr("data."+datasourceType+".test", "node_id", nodeIds[0].String()), + resource.TestCheckNoResourceAttr("data."+datasourceType+".test", "system_id"), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "deployed_config"), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "staged_config"), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "incremental_config", expectAsnChange), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "deployed_config", atLeast100Lines), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "staged_config", atLeast100Lines), + }, + }, + "leaf_0_post_change_by_system": { + preFunc: func(t testing.TB, ctx context.Context, client *apstra.TwoStageL3ClosClient) { + changeLeafAsn(t, ctx, nodeIds[0], bp) + }, + config: dataSourceBlueprintDeviceRenderedConfig{systemId: sysIds[0]}, + checks: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("data."+datasourceType+".test", "blueprint_id", bp.Id().String()), + resource.TestCheckNoResourceAttr("data."+datasourceType+".test", "node_id"), + resource.TestCheckResourceAttr("data."+datasourceType+".test", "system_id", sysIds[0].String()), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "deployed_config"), + resource.TestCheckResourceAttrSet("data."+datasourceType+".test", "staged_config"), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "incremental_config", expectAsnChange), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "deployed_config", atLeast100Lines), + resource.TestCheckResourceAttrWith("data."+datasourceType+".test", "staged_config", atLeast100Lines), + }, + }, + } + + // bpModificationWg delays modifications to the blueprint until pre-modification tests are complete + bpModificationWg := new(sync.WaitGroup) + + // testCaseStartWg ensures that no test case starts begins before all have had a chance + // to pile onto bpModificationWg + testCaseStartWg := new(sync.WaitGroup) + testCaseStartWg.Add(len(testCases)) + + for tName, tCase := range testCases { + t.Run(tName, func(t *testing.T) { + if tCase.config.nodeId == "" && tCase.config.systemId == "" { + testCaseStartWg.Done() + t.Skipf("skipping because node has no system assigned") + return + } + t.Parallel() + + if tCase.preFunc == nil { + bpModificationWg.Add(1) + testCaseStartWg.Done() + } else { + testCaseStartWg.Done() + bpModificationWg.Wait() + tCase.preFunc(t, ctx, bp) + } + + config := tCase.config.render(datasourceType, bp.Id().String()) + t.Logf("\n// ------ begin config ------%s// -------- end config ------\n\n", config) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: insecureProviderConfigHCL + config, + Check: resource.ComposeAggregateTestCheckFunc(tCase.checks...), + }, + }, + }) + + if tCase.preFunc == nil { + bpModificationWg.Done() // release test cases which will make changes + } + }) + } +} diff --git a/apstra/export_test.go b/apstra/export_test.go index 9eceb290..6aab967c 100644 --- a/apstra/export_test.go +++ b/apstra/export_test.go @@ -3,11 +3,14 @@ package tfapstra import ( "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) var ( + DataSourceBlueprintNodeConfig = dataSourceBlueprintNodeConfig{} + ResourceAgentProfile = resourceAgentProfile{} ResourceAsnPool = resourceAsnPool{} ResourceDatacenterConnectivityTemplateInterface = resourceDatacenterConnectivityTemplateInterface{} @@ -38,6 +41,20 @@ var ( ResourceVniPool = resourceVniPool{} ) +func DatasourceName(ctx context.Context, d datasource.DataSource) string { + var pMdReq provider.MetadataRequest + var pMdResp provider.MetadataResponse + NewProvider().Metadata(ctx, pMdReq, &pMdResp) + + var dMdReq datasource.MetadataRequest + var dMdResp datasource.MetadataResponse + + dMdReq.ProviderTypeName = pMdResp.TypeName + d.Metadata(ctx, dMdReq, &dMdResp) + + return dMdResp.TypeName +} + func ResourceName(ctx context.Context, r resource.Resource) string { var pMdReq provider.MetadataRequest var pMdResp provider.MetadataResponse diff --git a/apstra/provider.go b/apstra/provider.go index 5316b9ee..ca2f84b8 100644 --- a/apstra/provider.go +++ b/apstra/provider.go @@ -520,6 +520,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func() datasource.DataSource { return &dataSourceBlueprintIbaWidgets{} }, func() datasource.DataSource { return &dataSourceBlueprintIbaDashboard{} }, func() datasource.DataSource { return &dataSourceBlueprintIbaDashboards{} }, + func() datasource.DataSource { return &dataSourceBlueprintNodeConfig{} }, func() datasource.DataSource { return &dataSourceBlueprints{} }, func() datasource.DataSource { return &dataSourceConfiglet{} }, func() datasource.DataSource { return &dataSourceConfiglets{} }, diff --git a/apstra/test_helpers_test.go b/apstra/test_helpers_test.go index 615b4d77..938c9287 100644 --- a/apstra/test_helpers_test.go +++ b/apstra/test_helpers_test.go @@ -67,11 +67,11 @@ func stringPtrOrNull[S ~string](in *S) string { return fmt.Sprintf(`%q`, *in) } -func stringOrNull(in string) string { +func stringOrNull[S ~string](in S) string { if in == "" { return "null" } - return `"` + in + `"` + return fmt.Sprintf("%q", in) } func ipOrNull(in net.IP) string { diff --git a/apstra/test_utils/blueprint.go b/apstra/test_utils/blueprint.go index ad05db0d..1d933378 100644 --- a/apstra/test_utils/blueprint.go +++ b/apstra/test_utils/blueprint.go @@ -264,6 +264,97 @@ func BlueprintG(t testing.TB, ctx context.Context, cleanup bool) *apstra.TwoStag return bpClient } +//func BlueprintH(t testing.TB, ctx context.Context, cleanup bool) *apstra.TwoStageL3ClosClient { +//} + +func BlueprintI(t testing.TB, ctx context.Context) *apstra.TwoStageL3ClosClient { + t.Helper() + + client := GetTestClient(t, ctx) + + bpId, err := client.CreateBlueprintFromTemplate(ctx, &apstra.CreateBlueprintFromTemplateRequest{ + RefDesign: apstra.RefDesignTwoStageL3Clos, + Label: acctest.RandString(6), + TemplateId: "L3_Collapsed_ESI", + }) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, client.DeleteBlueprint(ctx, bpId)) }) + + bpClient, err := client.NewTwoStageL3ClosClient(ctx, bpId) + require.NoError(t, err) + + // assign leaf interface maps + leafIds := GetSystemIds(t, ctx, bpClient, "leaf") + mappings := make(apstra.SystemIdToInterfaceMapAssignment, len(leafIds)) + for _, leafId := range leafIds { + mappings[leafId.String()] = "Juniper_vQFX__AOS-7x10-Leaf" + } + err = bpClient.SetInterfaceMapAssignments(ctx, mappings) + require.NoError(t, err) + + // set leaf loopback pool + err = bpClient.SetResourceAllocation(ctx, &apstra.ResourceGroupAllocation{ + ResourceGroup: apstra.ResourceGroup{ + Type: apstra.ResourceTypeIp4Pool, + Name: apstra.ResourceGroupNameLeafIp4, + }, + PoolIds: []apstra.ObjectId{"Private-10_0_0_0-8"}, + }) + require.NoError(t, err) + + // set leaf-leaf pool + err = bpClient.SetResourceAllocation(ctx, &apstra.ResourceGroupAllocation{ + ResourceGroup: apstra.ResourceGroup{ + Type: apstra.ResourceTypeIp4Pool, + Name: apstra.ResourceGroupNameLeafLeafIp4, + }, + PoolIds: []apstra.ObjectId{"Private-10_0_0_0-8"}, + }) + require.NoError(t, err) + + // set leaf ASN pool + err = bpClient.SetResourceAllocation(ctx, &apstra.ResourceGroupAllocation{ + ResourceGroup: apstra.ResourceGroup{ + Type: apstra.ResourceTypeAsnPool, + Name: apstra.ResourceGroupNameLeafAsn, + }, + PoolIds: []apstra.ObjectId{"Private-64512-65534"}, + }) + require.NoError(t, err) + + // set VN VNI pool + err = bpClient.SetResourceAllocation(ctx, &apstra.ResourceGroupAllocation{ + ResourceGroup: apstra.ResourceGroup{ + Type: apstra.ResourceTypeVniPool, + Name: apstra.ResourceGroupNameEvpnL3Vni, + }, + PoolIds: []apstra.ObjectId{"Default-10000-20000"}, + }) + require.NoError(t, err) + + // set VN VNI pool + err = bpClient.SetResourceAllocation(ctx, &apstra.ResourceGroupAllocation{ + ResourceGroup: apstra.ResourceGroup{ + Type: apstra.ResourceTypeVniPool, + Name: apstra.ResourceGroupNameVxlanVnIds, + }, + PoolIds: []apstra.ObjectId{"Default-10000-20000"}, + }) + require.NoError(t, err) + + // commit + bpStatus, err := client.GetBlueprintStatus(ctx, bpClient.Id()) + require.NoError(t, err) + _, err = client.DeployBlueprint(ctx, &apstra.BlueprintDeployRequest{ + Id: bpClient.Id(), + Description: "initial commit in test: " + t.Name(), + Version: bpStatus.Version, + }) + require.NoError(t, err) + + return bpClient +} + func FfBlueprintA(t testing.TB, ctx context.Context) *apstra.FreeformClient { t.Helper() diff --git a/apstra/test_utils/test_utils.go b/apstra/test_utils/test_utils.go index 9db83702..c117693b 100644 --- a/apstra/test_utils/test_utils.go +++ b/apstra/test_utils/test_utils.go @@ -15,6 +15,7 @@ import ( "github.com/Juniper/terraform-provider-apstra/apstra/constants" "github.com/Juniper/terraform-provider-apstra/apstra/utils" "github.com/hashicorp/hcl/v2/hclsimple" + "github.com/stretchr/testify/require" ) const ( @@ -101,3 +102,36 @@ func TestCfgFileToEnv(t testing.TB) { t.Setenv(constants.EnvTlsNoVerify, strconv.FormatBool(testCfg.TlsValidationDisabled)) } + +func GetSystemIds(t testing.TB, ctx context.Context, bp *apstra.TwoStageL3ClosClient, role string) map[string]apstra.ObjectId { + t.Helper() + + leafQuery := new(apstra.PathQuery). + SetClient(bp.Client()). + SetBlueprintId(bp.Id()). + SetBlueprintType(apstra.BlueprintTypeStaging). + Node([]apstra.QEEAttribute{ + apstra.NodeTypeSystem.QEEAttribute(), + {"role", apstra.QEStringVal(role)}, + {"name", apstra.QEStringVal("n_system")}, + }) + + var leafQueryResult struct { + Items []struct { + System struct { + Id apstra.ObjectId `json:"id"` + Label string `json:"label"` + } `json:"n_system"` + } `json:"items"` + } + + err := leafQuery.Do(ctx, &leafQueryResult) + require.NoError(t, err) + + result := make(map[string]apstra.ObjectId, len(leafQueryResult.Items)) + for _, item := range leafQueryResult.Items { + result[item.System.Label] = item.System.Id + } + + return result +} diff --git a/go.mod b/go.mod index 97ee777a..70e3460f 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-20241010000234-c27c2a93c8cc + github.com/Juniper/apstra-go-sdk v0.0.0-20241017000354-6a9a100626cc 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 b9ebc1af..ba377bc8 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-20241010000234-c27c2a93c8cc h1:RjkmXaPqbCWNWc7QoS2O71dh+hNideqLWkY5fLgHGls= -github.com/Juniper/apstra-go-sdk v0.0.0-20241010000234-c27c2a93c8cc/go.mod h1:qXNVTdnVa40aMTOsBTnKoFNYT5ftga2NAkGJhx4o6bY= +github.com/Juniper/apstra-go-sdk v0.0.0-20241017000354-6a9a100626cc h1:qOsOL6JdVA8C1Y9xso8U2bbuzYdo2jXp0u8AHdiEcok= +github.com/Juniper/apstra-go-sdk v0.0.0-20241017000354-6a9a100626cc/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= From 0483840c57c2d03b3cbabc019ae811187c925220 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 17 Oct 2024 22:17:47 -0400 Subject: [PATCH 2/3] fumpt, docs --- ...source_blueprint_device_rendered_config.go | 7 +- ...device_rendered_config_integration_test.go | 11 +-- .../blueprint_device_rendered_config.md | 88 +++++++++++++++++++ .../example.tf | 54 ++++++++++++ 4 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 docs/data-sources/blueprint_device_rendered_config.md create mode 100644 examples/data-sources/apstra_blueprint_device_rendered_config/example.tf diff --git a/apstra/data_source_blueprint_device_rendered_config.go b/apstra/data_source_blueprint_device_rendered_config.go index f64b2051..c741c9b0 100644 --- a/apstra/data_source_blueprint_device_rendered_config.go +++ b/apstra/data_source_blueprint_device_rendered_config.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/Juniper/apstra-go-sdk/apstra" "github.com/Juniper/apstra-go-sdk/apstra/enum" "github.com/Juniper/terraform-provider-apstra/apstra/blueprint" @@ -12,8 +13,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ datasource.DataSourceWithConfigure = &dataSourceBlueprintNodeConfig{} -var _ datasourceWithSetClient = &dataSourceBlueprintNodeConfig{} +var ( + _ datasource.DataSourceWithConfigure = &dataSourceBlueprintNodeConfig{} + _ datasourceWithSetClient = &dataSourceBlueprintNodeConfig{} +) type dataSourceBlueprintNodeConfig struct { client *apstra.Client diff --git a/apstra/data_source_blueprint_device_rendered_config_integration_test.go b/apstra/data_source_blueprint_device_rendered_config_integration_test.go index 01abb622..9f687e8a 100644 --- a/apstra/data_source_blueprint_device_rendered_config_integration_test.go +++ b/apstra/data_source_blueprint_device_rendered_config_integration_test.go @@ -6,17 +6,18 @@ import ( "bufio" "context" "fmt" - "github.com/Juniper/apstra-go-sdk/apstra" - tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" - testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/stretchr/testify/require" "math" "math/rand/v2" "strconv" "strings" "sync" "testing" + + "github.com/Juniper/apstra-go-sdk/apstra" + tfapstra "github.com/Juniper/terraform-provider-apstra/apstra" + testutils "github.com/Juniper/terraform-provider-apstra/apstra/test_utils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" ) const dataSourceBlueprintDeviceRenderedConfigHCL = ` diff --git a/docs/data-sources/blueprint_device_rendered_config.md b/docs/data-sources/blueprint_device_rendered_config.md new file mode 100644 index 00000000..ee69a3b5 --- /dev/null +++ b/docs/data-sources/blueprint_device_rendered_config.md @@ -0,0 +1,88 @@ +--- +page_title: "apstra_blueprint_device_rendered_config Data Source - terraform-provider-apstra" +subcategory: "Reference Design: Shared" +description: |- + This data source retrieves rendered device configuration for a system in a Blueprint. +--- + +# apstra_blueprint_device_rendered_config (Data Source) + +This data source retrieves rendered device configuration for a system in a Blueprint. + + +## Example Usage + +```terraform +# Device configuraiton details can be fetched within within a blueprint +# either by system node ID (regardless of whether a switch is assigned) +# or by assigned system ID (device key / serial number / MAC address): + +// look up the details of spine 1 +data "apstra_datacenter_system" "example" { + blueprint_id = local.blueprint_id + name = "spine1" +} + +# retrieve the config details +data "apstra_blueprint_device_rendered_config" "example" { + blueprint_id = local.blueprint_id + node_id = data.apstra_datacenter_system.example.id + # system_id = "525400E365A5" // specify either the system graph node ID or the assigned switch serial number +} + +output "rendered_config" { value = data.apstra_blueprint_device_rendered_config.example } + +# The output looks like this: +# +# rendered_config = { +# "blueprint_id" = "d3336749-88c9-4922-8f61-043198664840" +# "deployed_config" = <<-EOT +# system { +# host-name spine1; +# } +# interfaces { +# replace: xe-0/0/0 { +# description "facing_l2-virtual-001-leaf1:xe-0/0/0"; +# mtu 9216; +# <<<<<>>>>> +# EOT +# "incremental_config" = <<-EOT +# +# [routing-options] +# - autonomous-system 64512; +# + autonomous-system 64511; +# +# EOT +# "node_id" = "GCmJI_Tl47TkOls2vg" +# "staged_config" = <<-EOT +# system { +# host-name spine1; +# } +# interfaces { +# replace: xe-0/0/0 { +# description "facing_l2-virtual-001-leaf1:xe-0/0/0"; +# mtu 9216; +# <<<<<>>>>> +# EOT +# "system_id" = tostring(null) +# } +# +``` + + +## Schema + +### Required + +- `blueprint_id` (String) Apstra Blueprint ID. + +### Optional + +- `node_id` (String) Apstra ID of the System (spine, leaf, etc...) node. Required when `system_id` is omitted. +- `system_id` (String) Apstra ID (serial number) for the System (Managed Device), as found in Devices -> Managed Devices in the GUI. Required when `node_id` is omitted. + +### Read-Only + +- `deployed_config` (String) Deployed device configuration. +- `incremental_config` (String) Incremental device configuration. +- `staged_config` (String) Staged device configuration. diff --git a/examples/data-sources/apstra_blueprint_device_rendered_config/example.tf b/examples/data-sources/apstra_blueprint_device_rendered_config/example.tf new file mode 100644 index 00000000..7bc0bde4 --- /dev/null +++ b/examples/data-sources/apstra_blueprint_device_rendered_config/example.tf @@ -0,0 +1,54 @@ +# Device configuraiton details can be fetched within within a blueprint +# either by system node ID (regardless of whether a switch is assigned) +# or by assigned system ID (device key / serial number / MAC address): + +// look up the details of spine 1 +data "apstra_datacenter_system" "example" { + blueprint_id = local.blueprint_id + name = "spine1" +} + +# retrieve the config details +data "apstra_blueprint_device_rendered_config" "example" { + blueprint_id = local.blueprint_id + node_id = data.apstra_datacenter_system.example.id + # system_id = "525400E365A5" // specify either the system graph node ID or the assigned switch serial number +} + +output "rendered_config" { value = data.apstra_blueprint_device_rendered_config.example } + +# The output looks like this: +# +# rendered_config = { +# "blueprint_id" = "d3336749-88c9-4922-8f61-043198664840" +# "deployed_config" = <<-EOT +# system { +# host-name spine1; +# } +# interfaces { +# replace: xe-0/0/0 { +# description "facing_l2-virtual-001-leaf1:xe-0/0/0"; +# mtu 9216; +# <<<<<>>>>> +# EOT +# "incremental_config" = <<-EOT +# +# [routing-options] +# - autonomous-system 64512; +# + autonomous-system 64511; +# +# EOT +# "node_id" = "GCmJI_Tl47TkOls2vg" +# "staged_config" = <<-EOT +# system { +# host-name spine1; +# } +# interfaces { +# replace: xe-0/0/0 { +# description "facing_l2-virtual-001-leaf1:xe-0/0/0"; +# mtu 9216; +# <<<<<>>>>> +# EOT +# "system_id" = tostring(null) +# } +# From 11fb9c5ed878a8ea948038a5628c959572a5bbd0 Mon Sep 17 00:00:00 2001 From: Chris Marget Date: Thu, 17 Oct 2024 22:21:10 -0400 Subject: [PATCH 3/3] fix problems identified by `go vet` --- apstra/blueprint/device_rendered_config.go | 4 ++-- apstra/test_utils/test_utils.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apstra/blueprint/device_rendered_config.go b/apstra/blueprint/device_rendered_config.go index 841a46de..006e86b4 100644 --- a/apstra/blueprint/device_rendered_config.go +++ b/apstra/blueprint/device_rendered_config.go @@ -12,8 +12,8 @@ type RenderedConfig struct { BlueprintId types.String `tfsdk:"blueprint_id"` SystemId types.String `tfsdk:"system_id"` NodeId types.String `tfsdk:"node_id"` - StagedCfg types.String `tfsdk:"staged_config""` - DeployedCfg types.String `tfsdk:"deployed_config""` + StagedCfg types.String `tfsdk:"staged_config"` + DeployedCfg types.String `tfsdk:"deployed_config"` Incremental types.String `tfsdk:"incremental_config"` } diff --git a/apstra/test_utils/test_utils.go b/apstra/test_utils/test_utils.go index c117693b..30deae67 100644 --- a/apstra/test_utils/test_utils.go +++ b/apstra/test_utils/test_utils.go @@ -112,8 +112,8 @@ func GetSystemIds(t testing.TB, ctx context.Context, bp *apstra.TwoStageL3ClosCl SetBlueprintType(apstra.BlueprintTypeStaging). Node([]apstra.QEEAttribute{ apstra.NodeTypeSystem.QEEAttribute(), - {"role", apstra.QEStringVal(role)}, - {"name", apstra.QEStringVal("n_system")}, + {Key: "role", Value: apstra.QEStringVal(role)}, + {Key: "name", Value: apstra.QEStringVal("n_system")}, }) var leafQueryResult struct {