From 30465692fae8af9ea2dab36a7d9ff3af8b5e1e4e Mon Sep 17 00:00:00 2001 From: akinross Date: Wed, 6 Nov 2024 09:32:08 +0100 Subject: [PATCH] [minor_change] Add resource and datasource for mso_template --- examples/template/main.tf | 86 +++ mso/datasource_mso_template.go | 84 +++ mso/datasource_mso_template_test.go | 143 +++++ mso/provider.go | 2 + mso/resource_mso_template.go | 491 +++++++++++++++ mso/resource_mso_template_test.go | 819 ++++++++++++++++++++++++++ website/docs/d/template.html.markdown | 37 ++ website/docs/r/template.html.markdown | 43 ++ 8 files changed, 1705 insertions(+) create mode 100644 examples/template/main.tf create mode 100644 mso/datasource_mso_template.go create mode 100644 mso/datasource_mso_template_test.go create mode 100644 mso/resource_mso_template.go create mode 100644 mso/resource_mso_template_test.go create mode 100644 website/docs/d/template.html.markdown create mode 100644 website/docs/r/template.html.markdown diff --git a/examples/template/main.tf b/examples/template/main.tf new file mode 100644 index 00000000..18737ed5 --- /dev/null +++ b/examples/template/main.tf @@ -0,0 +1,86 @@ +terraform { + required_providers { + mso = { + source = "CiscoDevNet/mso" + } + } +} + +provider "mso" { + username = "" # + password = "" # + url = "" # + insecure = true +} + +data "mso_site" "site_1" { + name = "example_site_1" +} + +data "mso_site" "site_2" { + name = "example_site_2" +} + +data "mso_tenant" "example_tenant" { + name = "example_tenant" +} + +# tenant template example + +resource "mso_template" "tenant_template" { + template_name = "tenant_template" + template_type = "tenant" + tenant_id = data.mso_tenant.example_tenant.id + sites = [data.mso_site.site_1.id, data.mso_site.site_2.id] +} + +# l3out template example + +resource "mso_template" "l3out_template" { + template_name = "l3out_template" + template_type = "l3out" + tenant_id = data.mso_tenant.example_tenant.id + sites = [data.mso_site.site_1.id] +} + +# fabric policy template example + +resource "mso_template" "fabric_policy_template" { + template_name = "fabric_policy_template" + template_type = "fabric_policy" + sites = [data.mso_site.site_1.id, data.mso_site.site_2.id] +} + +# fabric resource template example + +resource "mso_template" "fabric_resource_template" { + template_name = "fabric_resource_template" + template_type = "fabric_resource" + sites = [data.mso_site.site_1.id, data.mso_site.site_2.id] +} + +# monitoring tenant template example + +resource "mso_template" "monitoring_tenant_template" { + template_name = "monitoring_tenant_template" + template_type = "monitoring_tenant" + tenant_id = data.mso_tenant.example_tenant.id + sites = [data.mso_site.site_1.id] +} + +# monitoring access template example + +resource "mso_template" "monitoring_access_template" { + template_name = "monitoring_access_template" + template_type = "monitoring_access" + sites = [data.mso_site.site_1.id] +} + +# service device template example + +resource "mso_template" "service_device_template" { + template_name = "service_device_template" + template_type = "service_device" + tenant_id = data.mso_tenant.example_tenant.id + sites = [data.mso_site.site_1.id, data.mso_site.site_2.id] +} diff --git a/mso/datasource_mso_template.go b/mso/datasource_mso_template.go new file mode 100644 index 00000000..310b2b83 --- /dev/null +++ b/mso/datasource_mso_template.go @@ -0,0 +1,84 @@ +package mso + +import ( + "fmt" + "log" + + "github.com/ciscoecosystem/mso-go-client/client" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func datasourceMSOTemplate() *schema.Resource { + return &schema.Resource{ + + Read: datasourceMSOTemplateRead, + + SchemaVersion: version, + + Schema: (map[string]*schema.Schema{ + "template_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + "template_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + "template_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "tenant", + "l3out", + "fabric_policy", + "fabric_resource", + "monitoring_tenant", + "monitoring_access", + "service_device", + }, false), + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "sites": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }), + } +} + +func datasourceMSOTemplateRead(d *schema.ResourceData, m interface{}) error { + log.Println("[DEBUG] MSO Template Datasource: Beginning Read") + + msoClient := m.(*client.Client) + id := d.Get("template_id").(string) + name := d.Get("template_name").(string) + templateType := d.Get("template_type").(string) + + if id == "" && name == "" { + return fmt.Errorf("either `template_id` or `template_name` must be provided") + } else if id != "" && name != "" { + return fmt.Errorf("only one of `template_id` or `template_name` must be provided") + } else if name != "" && templateType == "" { + return fmt.Errorf("`template_type` must be provided when `template_name` is provided") + } + + ndoTemplate := ndoTemplate{msoClient: msoClient, id: id, templateName: name, templateType: templateType} + err := ndoTemplate.getTemplate(true) + if err != nil { + return err + } + ndoTemplate.SetToSchema(d) + d.Set("template_id", d.Id()) + log.Println("[DEBUG] MSO Template Datasource: Read Completed", d.Id()) + return nil + +} diff --git a/mso/datasource_mso_template_test.go b/mso/datasource_mso_template_test.go new file mode 100644 index 00000000..9cda222c --- /dev/null +++ b/mso/datasource_mso_template_test.go @@ -0,0 +1,143 @@ +package mso + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccMSOTemplateDatasourceTenantErrors(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: No template_id or template_name provided in Template configuration") }, + Config: testAccMSOTemplateDatasourceErrorNoIdOrNameConfig(), + ExpectError: regexp.MustCompile("either `template_id` or `template_name` must be provided"), + }, + { + PreConfig: func() { fmt.Println("Test: No template_type with name provided in Template configuration") }, + Config: testAccMSOTemplateDatasourceErrorNoTypeConfig(), + ExpectError: regexp.MustCompile("`template_type` must be provided when `template_name` is provided"), + }, + { + PreConfig: func() { fmt.Println("Test: Both template_id and template_name provided in Template configuration") }, + Config: testAccMSOTemplateDatasourceErrorIdAndNameConfig(), + ExpectError: regexp.MustCompile("only one of `template_id` or `template_name` must be provided"), + }, + { + PreConfig: func() { fmt.Println("Test: Non existing template name provided in Template configuration") }, + Config: testAccMSOTemplateDatasourceErrorNonExistingConfig(), + ExpectError: regexp.MustCompile("Template with name 'non_existing_template_name' not found."), + }, + }, + }) +} + +func TestAccMSOTemplateDatasourceTenantName(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: Tenant template with name and type provided in Template configuration") }, + Config: testAccMSOTemplateDatasourceNameAndTypeConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "data.mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1, msoTemplateSiteName2}, + }, + false, + ), + ), + }, + }, + }) +} + +func TestAccMSOTemplateDatasourceTenantId(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: Tenant template with id provided in Template configuration") }, + Config: testAccMSOTemplateDatasourceIdConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "data.mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1, msoTemplateSiteName2}, + }, + false, + ), + ), + }, + }, + }) +} + +func testAccMSOTemplateDatasourceNameAndTypeConfig() string { + return fmt.Sprintf(`%s + data "mso_template" "template_tenant" { + template_name = mso_template.template_tenant.template_name + template_type = "tenant" + } + `, testAccMSOTemplateResourceTenanTwoSitesConfig()) +} + +func testAccMSOTemplateDatasourceIdConfig() string { + return fmt.Sprintf(`%s + data "mso_template" "template_tenant" { + template_id = mso_template.template_tenant.id + } + `, testAccMSOTemplateResourceTenanTwoSitesConfig()) +} + +func testAccMSOTemplateDatasourceErrorNoIdOrNameConfig() string { + return fmt.Sprintf(` + data "mso_template" "template_tenant" { + template_type = "tenant" + } + `) +} + +func testAccMSOTemplateDatasourceErrorNoTypeConfig() string { + return fmt.Sprintf(` + data "mso_template" "template_tenant" { + template_name = "non_existing_template_name" + } + `) +} + +func testAccMSOTemplateDatasourceErrorIdAndNameConfig() string { + return fmt.Sprintf(` + data "mso_template" "template_tenant" { + template_id = "non_existing_template_id" + template_name = "non_existing_template_name" + template_type = "tenant" + } + `) +} + +func testAccMSOTemplateDatasourceErrorNonExistingConfig() string { + return fmt.Sprintf(` + data "mso_template" "template_tenant" { + template_name = "non_existing_template_name" + template_type = "tenant" + } + `) +} diff --git a/mso/provider.go b/mso/provider.go index 8f511389..f773504e 100644 --- a/mso/provider.go +++ b/mso/provider.go @@ -116,6 +116,7 @@ func Provider() terraform.ResourceProvider { "mso_system_config": resourceMSOSystemConfig(), "mso_schema_site_contract_service_graph": resourceMSOSchemaSiteContractServiceGraph(), "mso_schema_site_contract_service_graph_listener": resourceMSOSchemaSiteContractServiceGraphListener(), + "mso_template": resourceMSOTemplate(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -172,6 +173,7 @@ func Provider() terraform.ResourceProvider { "mso_rest": datasourceMSORest(), "mso_schema_site_contract_service_graph": dataSourceMSOSchemaSiteContractServiceGraph(), "mso_schema_site_contract_service_graph_listener": dataSourceMSOSchemaSiteContractServiceGraphListener(), + "mso_template": datasourceMSOTemplate(), }, ConfigureFunc: configureClient, diff --git a/mso/resource_mso_template.go b/mso/resource_mso_template.go new file mode 100644 index 00000000..17063fd5 --- /dev/null +++ b/mso/resource_mso_template.go @@ -0,0 +1,491 @@ +package mso + +import ( + "errors" + "fmt" + "log" + "strings" + + "github.com/ciscoecosystem/mso-go-client/client" + "github.com/ciscoecosystem/mso-go-client/container" + "github.com/ciscoecosystem/mso-go-client/models" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func resourceMSOTemplate() *schema.Resource { + return &schema.Resource{ + Create: resourceMSOTemplateCreate, + Read: resourceMSOTemplateRead, + Update: resourceMSOTemplateUpdate, + Delete: resourceMSOTemplateDelete, + Importer: &schema.ResourceImporter{ + State: resourceMSOTemplateImport, + }, + SchemaVersion: 1, + Schema: map[string]*schema.Schema{ + "template_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + "template_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "tenant", + "l3out", + "fabric_policy", + "fabric_resource", + "monitoring_tenant", + "monitoring_access", + "service_device", + }, false), + }, + "tenant_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "sites": { + Type: schema.TypeList, // Set cannot not be used because the order of sites is important for deletion, since full list replacement is not supported in API + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + // ValidateFunc not supported on type list / set else duplication could be caught with duplication.ListOfUniqueStrings + // │ Error: Internal validation of the provider failed! This is always a bug + // │ with the provider itself, and not a user issue. Please report + // │ this bug: + // │ + // │ 1 error occurred: + // │ * resource mso_template: sites: ValidateFunc is not yet supported on lists or sets. + }, + }, + CustomizeDiff: func(diff *schema.ResourceDiff, v interface{}) error { + oldSites, newSites := diff.GetChange("sites") + + sites := convertToListOfStrings(newSites.([]interface{})) + + if len(oldSites.([]interface{})) != len(newSites.([]interface{})) { + return nil + } + + for _, oldSite := range oldSites.([]interface{}) { + if !valueInSliceofStrings(oldSite.(string), sites) { + return nil + } + } + diff.SetNew("sites", oldSites) + + return nil + }, + } +} + +type ndoTemplateType struct { + templateType string // templateType in payload + templateTypeContainer string // templateType container in payload + tenant bool // tenant required + siteAmount int // 1 = 1 site, 2 = multiple sites + templateContainer bool // configuration is set in template container in payload +} + +var ndoTemplateTypes = map[string]ndoTemplateType{ + "tenant": ndoTemplateType{ + templateType: "tenantPolicy", + templateTypeContainer: "tenantPolicyTemplate", + tenant: true, + siteAmount: 2, + templateContainer: true, + }, + "l3out": ndoTemplateType{ + templateType: "l3out", + templateTypeContainer: "l3outTemplate", + tenant: true, + siteAmount: 1, + templateContainer: false, + }, + "fabric_policy": ndoTemplateType{ + templateType: "fabricPolicy", + templateTypeContainer: "fabricPolicyTemplate", + tenant: false, + siteAmount: 2, + templateContainer: true, + }, + "fabric_resource": ndoTemplateType{ + templateType: "fabricResource", + templateTypeContainer: "fabricResourceTemplate", + tenant: false, + siteAmount: 2, + templateContainer: true, + }, + "monitoring_tenant": ndoTemplateType{ + templateType: "monitoring", + templateTypeContainer: "monitoringTemplate", + tenant: true, + siteAmount: 1, + templateContainer: true, + }, + "monitoring_access": ndoTemplateType{ + templateType: "monitoring", + templateTypeContainer: "monitoringTemplate", + tenant: false, + siteAmount: 1, + templateContainer: true, + }, + "service_device": ndoTemplateType{ + templateType: "serviceDevice", + templateTypeContainer: "deviceTemplate", + tenant: true, + siteAmount: 2, + templateContainer: true, + }, +} + +type ndoTemplate struct { + id string + templateName string + templateType string + tenantId string + sites []string + msoClient *client.Client +} + +func (ndoTemplate *ndoTemplate) SetToSchema(d *schema.ResourceData) { + d.SetId(ndoTemplate.id) + d.Set("template_name", ndoTemplate.templateName) + d.Set("template_type", ndoTemplate.templateType) + d.Set("tenant_id", ndoTemplate.tenantId) + d.Set("sites", ndoTemplate.sites) +} + +func (ndoTemplate *ndoTemplate) validateConfig() []error { + errors := []error{} + + if ndoTemplate.tenantId != "" && !ndoTemplateTypes[ndoTemplate.templateType].tenant { + errors = append(errors, fmt.Errorf(fmt.Sprintf("Tenant cannot be attached to template of type %s.", ndoTemplate.templateType))) + } + if ndoTemplate.tenantId == "" && ndoTemplateTypes[ndoTemplate.templateType].tenant { + errors = append(errors, fmt.Errorf(fmt.Sprintf("Tenant is required for template of type %s.", ndoTemplate.templateType))) + } + if len(ndoTemplate.sites) == 0 && ndoTemplateTypes[ndoTemplate.templateType].siteAmount == 1 { + errors = append(errors, fmt.Errorf(fmt.Sprintf("Site is required for template of type %s.", ndoTemplate.templateType))) + } + if len(ndoTemplate.sites) > 1 && ndoTemplateTypes[ndoTemplate.templateType].siteAmount == 1 { + errors = append(errors, fmt.Errorf(fmt.Sprintf("Only one site is allowed for template of type %s.", ndoTemplate.templateType))) + } + duplicates := duplicatesInList(ndoTemplate.sites) + if len(duplicates) > 0 { + duplicatesErrors := []error{fmt.Errorf("Duplication found in the sites list")} + for _, site := range duplicates { + duplicatesErrors = append(duplicatesErrors, fmt.Errorf(fmt.Sprintf("Site %s is duplicated", site))) + } + return duplicatesErrors + } + + return errors +} + +func (ndoTemplate *ndoTemplate) ToMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "displayName": ndoTemplate.templateName, + "templateType": ndoTemplateTypes[ndoTemplate.templateType].templateType, + ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer: ndoTemplate.createTypeSpecificPayload(), + }, nil +} + +func (ndoTemplate *ndoTemplate) createTypeSpecificPayload() map[string]interface{} { + if ndoTemplate.templateType == "tenant" { + return map[string]interface{}{"template": map[string]interface{}{"tenantId": ndoTemplate.tenantId}, "sites": ndoTemplate.createSitesPayload()} + } else if ndoTemplate.templateType == "l3out" { + return map[string]interface{}{"tenantId": ndoTemplate.tenantId, "siteId": ndoTemplate.createSitesPayload()[0]["siteId"]} + } else if ndoTemplate.templateType == "fabric_policy" { + return map[string]interface{}{"sites": ndoTemplate.createSitesPayload()} + } else if ndoTemplate.templateType == "fabric_resource" { + return map[string]interface{}{"sites": ndoTemplate.createSitesPayload()} + } else if ndoTemplate.templateType == "monitoring_tenant" { + return map[string]interface{}{"template": map[string]interface{}{"mtType": "tenant", "tenant": ndoTemplate.tenantId}, "sites": ndoTemplate.createSitesPayload()} + } else if ndoTemplate.templateType == "monitoring_access" { + return map[string]interface{}{"template": map[string]interface{}{"mtType": "access"}, "sites": ndoTemplate.createSitesPayload()} + } else if ndoTemplate.templateType == "service_device" { + return map[string]interface{}{"template": map[string]interface{}{"tenantId": ndoTemplate.tenantId}, "sites": ndoTemplate.createSitesPayload()} + } + return nil +} + +func (ndoTemplate *ndoTemplate) createSitesPayload() []map[string]interface{} { + siteIds := []map[string]interface{}{} + for _, siteId := range ndoTemplate.sites { + siteIds = append(siteIds, ndoTemplate.createSitePayload(siteId)) + } + return siteIds +} + +func (ndoTemplate *ndoTemplate) createSitePayload(siteId string) map[string]interface{} { + return map[string]interface{}{"siteId": siteId} +} + +func (ndoTemplate *ndoTemplate) getTemplate(errorNotFound bool) error { + + if ndoTemplate.id == "" { + err := ndoTemplate.setTemplateId() + if err != nil { + return err + } + } + + cont, err := ndoTemplate.msoClient.GetViaURL(fmt.Sprintf("api/v1/templates/%s", ndoTemplate.id)) + if err != nil { + + // 404 scenario where the json response is not a valid json and cannot be parsed overwriting the response from mso in the client + if err.Error() == "invalid character 'p' after top-level value" { + return fmt.Errorf("Template ID %s invalid", ndoTemplate.id) + } + + // If template is not found, we can remove the id and try to find the template by name + if !errorNotFound && (cont.S("code").String() == "400" && strings.Contains(cont.S("message").String(), fmt.Sprintf("Template ID %s invalid", ndoTemplate.id))) { + ndoTemplate.id = "" + return nil + } + return err + } + + ndoTemplate.sites = []string{} + + ndoTemplate.templateName = models.StripQuotes(cont.S("displayName").String()) + templateType := models.StripQuotes(cont.S("templateType").String()) + + if templateType == "tenantPolicy" { + ndoTemplate.templateType = "tenant" + ndoTemplate.tenantId = models.StripQuotes(cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("template").S("tenantId").String()) + + } else if templateType == "l3out" { + ndoTemplate.templateType = "l3out" + ndoTemplate.tenantId = models.StripQuotes(cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("tenantId").String()) + ndoTemplate.sites = append(ndoTemplate.sites, models.StripQuotes(cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("siteId").String())) + + } else if templateType == "fabricPolicy" { + ndoTemplate.templateType = "fabric_policy" + + } else if templateType == "fabricResource" { + ndoTemplate.templateType = "fabric_resource" + + } else if templateType == "monitoring" { + ndoTemplate.templateType = "monitoring_access" + if models.StripQuotes(cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("template").S("mtType").String()) == "tenant" { + ndoTemplate.templateType = "monitoring_tenant" + ndoTemplate.tenantId = models.StripQuotes(cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("template").S("tenant").String()) + } + + } else if templateType == "serviceDevice" { + ndoTemplate.templateType = "service_device" + ndoTemplate.tenantId = models.StripQuotes(cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("template").S("tenantId").String()) + } + + if ndoTemplate.templateType != "l3out" { + if cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).Exists("sites") { + for _, site := range cont.S(ndoTemplateTypes[ndoTemplate.templateType].templateTypeContainer).S("sites").Data().([]interface{}) { + siteId := models.StripQuotes(site.(map[string]interface{})["siteId"].(string)) + ndoTemplate.sites = append(ndoTemplate.sites, siteId) + } + } + } + + return nil + +} + +func (ndoTemplate *ndoTemplate) setTemplateId() error { + cont, err := ndoTemplate.msoClient.GetViaURL(fmt.Sprintf("api/v1/templates/summaries")) + if err != nil { + return err + } + + templates, err := cont.Children() + if err != nil { + return err + } + + for _, template := range templates { + if ndoTemplate.templateName == models.StripQuotes(template.S("templateName").String()) && ndoTemplateTypes[ndoTemplate.templateType].templateType == models.StripQuotes(template.S("templateType").String()) { + ndoTemplate.id = models.StripQuotes(template.S("templateId").String()) + return nil + } + } + + return fmt.Errorf("Template with name '%s' not found.", ndoTemplate.templateName) +} + +func resourceMSOTemplateCreate(d *schema.ResourceData, m interface{}) error { + log.Println("[DEBUG] MSO Template Resource: Beginning Create", d.Id()) + msoClient := m.(*client.Client) + + ndoTemplate := ndoTemplate{ + msoClient: msoClient, + templateName: d.Get("template_name").(string), + templateType: d.Get("template_type").(string), + sites: getListOfStringsFromSchemaList(d, "sites"), + } + + if tenantId, ok := d.GetOk("tenant_id"); ok { + ndoTemplate.tenantId = tenantId.(string) + } + + validationErrors := ndoTemplate.validateConfig() + if len(validationErrors) > 0 { + d.SetId("") + return errors.Join(validationErrors...) + } + + response, err := msoClient.Save("api/v1/templates", &ndoTemplate) + if err != nil { + return err + } + + d.SetId(models.StripQuotes(response.S("templateId").String())) + log.Println("[DEBUG] MSO Template Resource: Create Completed", d.Id()) + return nil +} + +func resourceMSOTemplateRead(d *schema.ResourceData, m interface{}) error { + log.Println("[DEBUG] MSO Template Resource: Beginning Read", d.Id()) + msoClient := m.(*client.Client) + ndoTemplate := ndoTemplate{msoClient: msoClient, id: d.Id()} + err := ndoTemplate.getTemplate(false) + if err != nil { + return err + } + ndoTemplate.SetToSchema(d) + log.Println("[DEBUG] MSO Template Resource: Read Completed", d.Id()) + return nil +} + +func resourceMSOTemplateUpdate(d *schema.ResourceData, m interface{}) error { + log.Println("[DEBUG] MSO Template Resource: Beginning Update", d.Id()) + msoClient := m.(*client.Client) + + templateType := d.Get("template_type").(string) + templateName := d.Get("template_name").(string) + sites := getListOfStringsFromSchemaList(d, "sites") + + if ndoTemplateTypes[templateType].siteAmount == 1 && d.HasChange("sites") { + return fmt.Errorf("Cannot change site for template of type %s.", templateType) + } + + ndoTemplate := ndoTemplate{ + msoClient: msoClient, + templateName: templateName, + templateType: templateType, + sites: sites, + } + + if tenantId, ok := d.GetOk("tenant_id"); ok { + ndoTemplate.tenantId = tenantId.(string) + } + + validationErrors := ndoTemplate.validateConfig() + if len(validationErrors) > 0 { + return errors.Join(validationErrors...) + } + + payloadCon := container.New() + payloadCon.Array() + + if d.HasChange("template_name") { + err := addPatchPayloadToContainer(payloadCon, "replace", "/displayName", templateName) + if err != nil { + return err + } + } + + if d.HasChange("sites") { + // Replace operation is not supported for sites, so we need to remove and add sites individually + + oldSites, _ := d.GetChange("sites") + + // Reversed loop to remove sites from the end of the list first, to prevent index shifts with wrong deletes + for index := len(oldSites.([]interface{})) - 1; index >= 0; index-- { + if !valueInSliceofStrings(oldSites.([]interface{})[index].(string), sites) { + err := addPatchPayloadToContainer(payloadCon, "remove", fmt.Sprintf("/%s/sites/%d", ndoTemplateTypes[templateType].templateTypeContainer, index), nil) + if err != nil { + return err + } + } + } + + for _, site := range sites { + if !valueInSliceofStrings(site, convertToListOfStrings(oldSites.([]interface{}))) { + err := addPatchPayloadToContainer(payloadCon, "add", fmt.Sprintf("/%s/sites/-", ndoTemplateTypes[templateType].templateTypeContainer), ndoTemplate.createSitePayload(site)) + if err != nil { + return err + } + } + } + } + + err := doPatchRequest(msoClient, fmt.Sprintf("api/v1/templates/%s", d.Id()), payloadCon) + if err != nil { + return err + } + + log.Println("[DEBUG] MSO Template Resource: Updating Completed", d.Id()) + return nil +} + +func resourceMSOTemplateDelete(d *schema.ResourceData, m interface{}) error { + log.Println("[DEBUG] MSO Template Resource: Beginning Delete", d.Id()) + msoClient := m.(*client.Client) + + err := msoClient.DeletebyId(fmt.Sprintf("api/v1/templates/%s", d.Id())) + if err != nil { + return err + } + log.Println("[DEBUG] MSO Template Resource: Delete Completed", d.Id()) + d.SetId("") + return nil +} + +func resourceMSOTemplateImport(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + log.Println("[DEBUG] MSO Template Resource: Beginning Import", d.Id()) + msoClient := m.(*client.Client) + ndoTemplate := ndoTemplate{msoClient: msoClient, id: d.Id()} + err := ndoTemplate.getTemplate(true) + if err != nil { + return nil, err + } + ndoTemplate.SetToSchema(d) + log.Println("[DEBUG] MSO Template Resource: Import Completed", d.Id()) + return []*schema.ResourceData{d}, nil +} + +func getListOfStringsFromSchemaList(d *schema.ResourceData, key string) []string { + if values, ok := d.GetOk(key); ok { + return convertToListOfStrings(values.([]interface{})) + } + return nil +} + +func convertToListOfStrings(values []interface{}) []string { + result := []string{} + for _, item := range values { + result = append(result, item.(string)) + } + return result +} + +func duplicatesInList(list []string) []string { + duplicates := []string{} + set := make(map[string]int) + for index, item := range list { + if _, ok := set[item]; ok { + duplicates = append(duplicates, item) + } else { + set[item] = index + } + } + return duplicates +} diff --git a/mso/resource_mso_template_test.go b/mso/resource_mso_template_test.go new file mode 100644 index 00000000..83f1e448 --- /dev/null +++ b/mso/resource_mso_template_test.go @@ -0,0 +1,819 @@ +package mso + +import ( + "fmt" + "regexp" + "strconv" + "testing" + + "github.com/ciscoecosystem/mso-go-client/client" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +const msoTemplateTenantName = "tf_test_mso_template_tenant" +const msoTemplateSiteName1 = "ansible_test" +const msoTemplateSiteName2 = "ansible_test_2" + +var msoTemplateId string + +func TestAccMSOTemplateResourceTenant(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: Duplicate sites provided in Tenant Template configuration (error)") }, + Config: testAccMSOTemplateResourceTenantErrorDuplicateSitesConfig(), + ExpectError: regexp.MustCompile(`Duplication found in the sites list`), + }, + { + PreConfig: func() { fmt.Println("Test: No tenant provided in Tenant Template configuration (error)") }, + Config: testAccMSOTemplateResourceTenanErrorNoTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant is required for template of type tenant.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create Tenant Template without sites") }, + Config: testAccMSOTemplateResourceTenantConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{}, + }, + true, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Import the Tenant Template with no sites configuration") }, + ResourceName: "mso_template.template_tenant", + ImportState: true, + ImportStateId: msoTemplateId, + ImportStateVerify: true, + }, + { + PreConfig: func() { + fmt.Println("Test: Import the Tenant Template with no sites configuration with wrong ID (error)") + }, + ResourceName: "mso_template.template_tenant", + ImportState: true, + ImportStateId: "non_existing_template_id", + ImportStateVerify: true, + ExpectError: regexp.MustCompile("Template ID non_existing_template_id invalid"), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with 1 site") }, + Config: testAccMSOTemplateResourceTenanSiteAnsibleTestConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with 2 sites") }, + Config: testAccMSOTemplateResourceTenanTwoSitesConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1, msoTemplateSiteName2}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with reverse order of sites") }, + Config: testAccMSOTemplateResourceTenanTwoSitesReversedConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1, msoTemplateSiteName2}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with removal of site 2") }, + Config: testAccMSOTemplateResourceTenanSiteAnsibleTest2Config(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName2}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with change of site 2 to site 1") }, + Config: testAccMSOTemplateResourceTenanSiteAnsibleTestConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with removal of sites configuration") }, + Config: testAccMSOTemplateResourceTenantNoSitesConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template name") }, + Config: testAccMSOTemplateResourceTenantNameChangeConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant_changed", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the Tenant Template with duplicate sites (error)") }, + Config: testAccMSOTemplateResourceTenantErrorDuplicateSitesConfig(), + ExpectError: regexp.MustCompile(`Duplication found in the sites list`), + }, + { + PreConfig: func() { + fmt.Println("Test: Update the Tenant Template after manual removal from MSO") + msoClient := testAccProvider.Meta().(*client.Client) + err := msoClient.DeletebyId(fmt.Sprintf("api/v1/templates/%s", msoTemplateId)) + if err != nil { + t.Fatalf("Failed to manually delete template '%s': %v", msoTemplateId, err) + } + }, + Config: testAccMSOTemplateResourceTenantNameChangeConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_tenant", + &TemplateTest{ + TemplateName: "test_template_tenant_changed", + TemplateType: "tenant", + Tenant: msoTemplateTenantName, + Sites: []string{}, + }, + false, + ), + ), + }, + }, + }) +} + +type TemplateTest struct { + TemplateName string `json:",omitempty"` + TemplateType string `json:",omitempty"` + Tenant string `json:",omitempty"` + Sites []string `json:",omitempty"` +} + +func testAccMSOTemplateState(resourceName string, stateTemplate *TemplateTest, setmsoTemplateId bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rootModule, err := s.RootModule().Resources[resourceName] + if !err { + return fmt.Errorf("%v", err) + } + + if rootModule.Primary.ID == "" { + return fmt.Errorf("No ID is set for the template") + } + + // Set the ID for the template to global variable only when called from specific resource test + // This is to avoid setting the ID issues when data source test is called first + if setmsoTemplateId { + msoTemplateId = rootModule.Primary.ID + } + + if rootModule.Primary.Attributes["tenant_id"] == "" && stateTemplate.Tenant != "" { + return fmt.Errorf("No tenant ID is set for the template") + } else if stateTemplate.Tenant != "" { + tenantState, err := s.RootModule().Resources[fmt.Sprintf("mso_tenant.%s", stateTemplate.Tenant)] + if !err { + return fmt.Errorf("Tenant %s not found in state", stateTemplate.Tenant) + } + if tenantState.Primary.Attributes["display_name"] != stateTemplate.Tenant { + return fmt.Errorf("Tenant display name does not match, expected: %s, got: %s", stateTemplate.Tenant, tenantState.Primary.Attributes["display_name"]) + } + } + + if rootModule.Primary.Attributes["template_name"] != stateTemplate.TemplateName { + return fmt.Errorf("Template name does not match, expected: %s, got: %s", stateTemplate.TemplateName, rootModule.Primary.Attributes["template_name"]) + } + + if rootModule.Primary.Attributes["template_type"] != stateTemplate.TemplateType { + return fmt.Errorf("Template type does not match, expected: %s, got: %s", stateTemplate.TemplateType, rootModule.Primary.Attributes["template_type"]) + } + + if sites, ok := rootModule.Primary.Attributes["sites.#"]; ok { + if siteAmount, e := strconv.Atoi(sites); e != nil { + return fmt.Errorf("Could not convert sites amount to integer") + } else if siteAmount != len(stateTemplate.Sites) { + return fmt.Errorf("Amount of sites do not match, expected: %d, got: %d", len(stateTemplate.Sites), len(rootModule.Primary.Attributes["sites.#"])) + } + + for _, site := range stateTemplate.Sites { + siteState, err := s.RootModule().Resources[fmt.Sprintf("data.mso_site.%s", site)] + if !err { + return fmt.Errorf("Site %s not found in state", site) + } + if siteState.Primary.Attributes["name"] != site { + return fmt.Errorf("Site display name does not match, expected: %s, got: %s", site, siteState.Primary.Attributes["display_name"]) + } + } + } else { + if len(stateTemplate.Sites) != 0 { + return fmt.Errorf("Amount of sites do not match, expected: %d, got: 0", len(stateTemplate.Sites)) + } + } + + return nil + } +} + +func testSiteConfigAnsibleTest() string { + return fmt.Sprintf(` + data "mso_site" "%s" { + name = "%s" + } + `, msoTemplateSiteName1, msoTemplateSiteName1) +} + +func testSiteConfigAnsibleTest2() string { + return fmt.Sprintf(` + data "mso_site" "%s" { + name = "%s" + } + `, msoTemplateSiteName2, msoTemplateSiteName2) +} + +func testTenantConfig() string { + return fmt.Sprintf(` + %s%s + resource "mso_tenant" "%s" { + name = "%s" + display_name = "%s" + site_associations { + site_id = data.mso_site.%s.id + } + site_associations { + site_id = data.mso_site.%s.id + } + } + `, testSiteConfigAnsibleTest(), testSiteConfigAnsibleTest2(), msoTemplateTenantName, msoTemplateTenantName, msoTemplateTenantName, msoTemplateSiteName1, msoTemplateSiteName2) +} + +func testAccMSOTemplateResourceTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func testAccMSOTemplateResourceTenantNameChangeConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant_changed" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func testAccMSOTemplateResourceTenantNoSitesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + sites = [] + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func testAccMSOTemplateResourceTenanSiteAnsibleTestConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceTenanSiteAnsibleTest2Config() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName2) +} + +func testAccMSOTemplateResourceTenanTwoSitesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1, msoTemplateSiteName2) +} + +func testAccMSOTemplateResourceTenanTwoSitesReversedConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName2, msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceTenantErrorDuplicateSitesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1, msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceTenanErrorNoTenantConfig() string { + return fmt.Sprintf(`%s%s + resource "mso_template" "template_tenant" { + template_name = "test_template_tenant" + template_type = "tenant" + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testSiteConfigAnsibleTest(), testSiteConfigAnsibleTest2(), msoTemplateSiteName1, msoTemplateSiteName2) +} + +func TestAccMSOTemplateResourceL3out(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: No tenant provided in L3out Template configuration (error)") }, + Config: testAccMSOTemplateResourceL3outErrorNoTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant is required for template of type l3out.`), + }, + { + PreConfig: func() { fmt.Println("Test: No sites provided in L3out Template configuration (error)") }, + Config: testAccMSOTemplateResourceL3outErrorNoSitesConfig(), + ExpectError: regexp.MustCompile(`Site is required for template of type l3out.`), + }, + { + PreConfig: func() { fmt.Println("Test: Two sites provided in L3out Template configuration (error)") }, + Config: testAccMSOTemplateResourceL3outErrorTwositesConfig(), + ExpectError: regexp.MustCompile(`Only one site is allowed for template of type l3out.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create L3out Template with 1 site") }, + Config: testAccMSOTemplateResourceL3outConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_l3out", + &TemplateTest{ + TemplateName: "test_template_l3out", + TemplateType: "l3out", + Tenant: msoTemplateTenantName, + Sites: []string{msoTemplateSiteName1}, + }, + false, + ), + ), + }, + { + PreConfig: func() { fmt.Println("Test: Update the L3out Template with change of site 1 to site 2 (error)") }, + Config: testAccMSOTemplateResourceL3outErrorChangeSiteConfig(), + ExpectError: regexp.MustCompile(`Cannot change site for template of type l3out.`), + }, + }, + }) +} + +func testAccMSOTemplateResourceL3outConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_l3out" { + template_name = "test_template_l3out" + template_type = "l3out" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceL3outErrorNoTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_l3out" { + template_name = "test_template_l3out" + template_type = "l3out" + sites = [data.mso_site.%s.id] + } + `, testSiteConfigAnsibleTest(), msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceL3outErrorNoSitesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_l3out" { + template_name = "test_template_l3out" + template_type = "l3out" + tenant_id = mso_tenant.%s.id + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func testAccMSOTemplateResourceL3outErrorTwositesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_l3out" { + template_name = "test_template_l3out" + template_type = "l3out" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1, msoTemplateSiteName2) +} + +func testAccMSOTemplateResourceL3outErrorChangeSiteConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_l3out" { + template_name = "test_template_l3out" + template_type = "l3out" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName2) +} + +func TestAccMSOTemplateResourceFabricPolicy(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: No tenant provided in Fabric Policy Template configuration (error)") }, + Config: testAccMSOTemplateResourceFabricPolicyErrorTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant cannot be attached to template of type fabric_policy.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create Fabric Policy Template without sites") }, + Config: testAccMSOTemplateResourceFabricPolicyConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_fabric_policy", + &TemplateTest{ + TemplateName: "test_template_fabric_policy", + TemplateType: "fabric_policy", + Sites: []string{}, + }, + false, + ), + ), + }, + }, + }) +} + +func testAccMSOTemplateResourceFabricPolicyConfig() string { + return fmt.Sprintf(` + resource "mso_template" "template_fabric_policy" { + template_name = "test_template_fabric_policy" + template_type = "fabric_policy" + } + `) +} + +func testAccMSOTemplateResourceFabricPolicyErrorTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_fabric_policy" { + template_name = "test_template_fabric_policy" + template_type = "fabric_policy" + tenant_id = mso_tenant.%s.id + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func TestAccMSOTemplateResourceFabricResource(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: Tenant provided in Fabric Resource Template configuration (error)") }, + Config: testAccMSOTemplateResourceFabricResourceErrorTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant cannot be attached to template of type fabric_resource.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create Fabric Resource Template without sites") }, + Config: testAccMSOTemplateResourceFabricResourceConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_fabric_resource", + &TemplateTest{ + TemplateName: "test_template_fabric_resource", + TemplateType: "fabric_resource", + Sites: []string{}, + }, + false, + ), + ), + }, + }, + }) +} + +func testAccMSOTemplateResourceFabricResourceConfig() string { + return fmt.Sprintf(` + resource "mso_template" "template_fabric_resource" { + template_name = "test_template_fabric_resource" + template_type = "fabric_resource" + } + `) +} + +func testAccMSOTemplateResourceFabricResourceErrorTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_fabric_resource" { + template_name = "test_template_fabric_resource" + template_type = "fabric_resource" + tenant_id = mso_tenant.%s.id + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func TestAccMSOTemplateResourceMonitoringTenant(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: No tenant provided in Monitoring Tenant Template configuration (error)") }, + Config: testAccMSOTemplateResourceMonitoringTenantErrorNoTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant is required for template of type monitoring_tenant.`), + }, + { + PreConfig: func() { fmt.Println("Test: No site provided in Monitoring Tenant Template configuration (error)") }, + Config: testAccMSOTemplateResourceMonitoringTenantErrorNoSiteConfig(), + ExpectError: regexp.MustCompile(`Site is required for template of type monitoring_tenant.`), + }, + { + PreConfig: func() { fmt.Println("Test: Two sites provided in Monitoring Tenant Template configuration (error)") }, + Config: testAccMSOTemplateResourceMonitoringTenantErrorTwoSitesConfig(), + ExpectError: regexp.MustCompile(`Only one site is allowed for template of type monitoring_tenant.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create Monitoring Tenant Template with 1 site") }, + Config: testAccMSOTemplateResourceMonitoringTenantConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_monitoring_tenant", + &TemplateTest{ + TemplateName: "test_template_monitoring_tenant", + TemplateType: "monitoring_tenant", + Sites: []string{msoTemplateSiteName1}, + }, + false, + ), + ), + }, + }, + }) +} + +func testAccMSOTemplateResourceMonitoringTenantErrorNoTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_tenant" { + template_name = "test_template_monitoring_tenant" + template_type = "monitoring_tenant" + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceMonitoringTenantErrorNoSiteConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_tenant" { + template_name = "test_template_monitoring_tenant" + template_type = "monitoring_tenant" + tenant_id = mso_tenant.%s.id + } + `, testTenantConfig(), msoTemplateTenantName) +} + +func testAccMSOTemplateResourceMonitoringTenantErrorTwoSitesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_tenant" { + template_name = "test_template_monitoring_tenant" + template_type = "monitoring_tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1, msoTemplateSiteName2) +} + +func testAccMSOTemplateResourceMonitoringTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_tenant" { + template_name = "test_template_monitoring_tenant" + template_type = "monitoring_tenant" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1) +} + +func TestAccMSOTemplateResourceMonitoringAccess(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: Tenant provided in Monitoring Access Template configuration (error)") }, + Config: testAccMSOTemplateResourceMonitoringAccessErrorTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant cannot be attached to template of type monitoring_access.`), + }, + { + PreConfig: func() { fmt.Println("Test: No site provided in Monitoring Access Template configuration (error)") }, + Config: testAccMSOTemplateResourceMonitoringAccessErrorNoSiteConfig(), + ExpectError: regexp.MustCompile(`Site is required for template of type monitoring_access.`), + }, + { + PreConfig: func() { fmt.Println("Test: Two sites provided in Monitoring Access Template configuration (error)") }, + Config: testAccMSOTemplateResourceMonitoringAccessErrorTwoSitesConfig(), + ExpectError: regexp.MustCompile(`Only one site is allowed for template of type monitoring_access.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create Monitoring Access Template with 1 site") }, + Config: testAccMSOTemplateResourceMonitoringAccessConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_monitoring_access", + &TemplateTest{ + TemplateName: "test_template_monitoring_access", + TemplateType: "monitoring_access", + Sites: []string{msoTemplateSiteName1}, + }, + false, + ), + ), + }, + }, + }) +} + +func testAccMSOTemplateResourceMonitoringAccessErrorTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_access" { + template_name = "test_template_monitoring_access" + template_type = "monitoring_access" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1) +} + +func testAccMSOTemplateResourceMonitoringAccessErrorNoSiteConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_access" { + template_name = "test_template_monitoring_access" + template_type = "monitoring_access" + } + `, testTenantConfig()) +} + +func testAccMSOTemplateResourceMonitoringAccessErrorTwoSitesConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_access" { + template_name = "test_template_monitoring_access" + template_type = "monitoring_access" + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateSiteName1, msoTemplateSiteName2) +} + +func testAccMSOTemplateResourceMonitoringAccessConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_monitoring_access" { + template_name = "test_template_monitoring_access" + template_type = "monitoring_access" + sites = [data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateSiteName1) +} + +func TestAccMSOTemplateResourceServiceDevice(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: func() { fmt.Println("Test: No tenant provided in Service Device Template configuration (error)") }, + Config: testAccMSOTemplateResourceServiceDeviceErrorNoTenantConfig(), + ExpectError: regexp.MustCompile(`Tenant is required for template of type service_device.`), + }, + { + PreConfig: func() { fmt.Println("Test: Create Service Device Template with 2 sites") }, + Config: testAccMSOTemplateResourceServiceDeviceConfig(), + Check: resource.ComposeTestCheckFunc( + testAccMSOTemplateState( + "mso_template.template_service_device", + &TemplateTest{ + TemplateName: "test_template_service_device", + TemplateType: "service_device", + Sites: []string{msoTemplateSiteName1, msoTemplateSiteName2}, + }, + false, + ), + ), + }, + }, + }) +} + +func testAccMSOTemplateResourceServiceDeviceErrorNoTenantConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_service_device" { + template_name = "test_template_service_device" + template_type = "service_device" + } + `, testTenantConfig()) +} + +func testAccMSOTemplateResourceServiceDeviceConfig() string { + return fmt.Sprintf(`%s + resource "mso_template" "template_service_device" { + template_name = "test_template_service_device" + template_type = "service_device" + tenant_id = mso_tenant.%s.id + sites = [data.mso_site.%s.id, data.mso_site.%s.id] + } + `, testTenantConfig(), msoTemplateTenantName, msoTemplateSiteName1, msoTemplateSiteName2) +} diff --git a/website/docs/d/template.html.markdown b/website/docs/d/template.html.markdown new file mode 100644 index 00000000..43d603b0 --- /dev/null +++ b/website/docs/d/template.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "mso" +page_title: "MSO: mso_template" +sidebar_current: "docs-mso-data-source-template" +description: |- + Data source for MSO Template. +--- + +# mso_template # + +Data source for MSO Template. + +## Example Usage ## + +```hcl + +data "mso_template" "example_with_name" { + template_name = "tenant_template" + template_type = "tenant" +} + +data "mso_template" "example_with_id" { + template_id = "6718b46395400f3759523378" +} + +``` + +## Argument Reference ## + +* `template_id` - (Optional) The ID of the template. Mutually exclusive with `template_name`. +* `template_name` - (Optional) The name of the template. Mutually exclusive with `template_id`. +* `template_type` - (Optional) The type of the template. Allowed values are `tenant`, `l3out`, `fabric_policy`, `fabric_resource`, `monitoring_tenant`, `monitoring_access`, or `service_device`. Required when `template_name` is provided. + +## Attribute Reference ## + +* `tenant_id` - (Read-Only) The ID of the tenant associated with the template. +* `sites` - (Read-Only) A list of site names associated with the template. diff --git a/website/docs/r/template.html.markdown b/website/docs/r/template.html.markdown new file mode 100644 index 00000000..7ae6879c --- /dev/null +++ b/website/docs/r/template.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "mso" +page_title: "MSO: mso_template" +sidebar_current: "docs-mso-resource-template" +description: |- + Manages MSO Template +--- + +# mso_template # + +Manages MSO Template + +## Example Usage ## + +```hcl + +resource "mso_template" "tenant_template" { + template_name = "tenant_template" + template_type = "tenant" + tenant_id = data.mso_tenant.example_tenant.id + sites = [data.mso_site.site_1.id, data.mso_site.site_2.id] +} + +``` + +## Argument Reference ## + +* `template_name` - (Required) The name of the template. +* `template_type` - (Required) The type of the template. Allowed values are `tenant`, `l3out`, `fabric_policy`, `fabric_resource`, `monitoring_tenant`, `monitoring_access`, or `service_device`. +* `tenant_id` - (Optional) The ID of the tenant to associate with the template. +* `sites` - (Optional) A list of site IDs to associate with the template. + +## Attribute Reference ## + +The only attribute exported with this resource is `id`. Which is set to the ID of the template. + +## Importing ## + +An existing MSO Schema Template can be [imported][docs-import] into this resource via its ID/path, via the following command: [docs-import]: + +```bash +terraform import mso_template.tenant_template {id} +```