From 6f6970ca377e29ebec3d6d2a144a87399aab5da5 Mon Sep 17 00:00:00 2001 From: Samuel Mutel <12967891+smutel@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:58:33 +0200 Subject: [PATCH] feat: Add netbox_dcim_region resource --- .../resources/netbox_dcim_region/import.sh | 2 + .../resources/netbox_dcim_region/resource.tf | 82 ++++++ netbox/dcim/resource_netbox_dcim_region.go | 277 ++++++++++++++++++ .../dcim/resource_netbox_dcim_region_test.go | 130 ++++++++ netbox/internal/util/nested.go | 8 + netbox/provider.go | 1 + 6 files changed, 500 insertions(+) create mode 100644 examples/resources/netbox_dcim_region/import.sh create mode 100644 examples/resources/netbox_dcim_region/resource.tf create mode 100644 netbox/dcim/resource_netbox_dcim_region.go create mode 100644 netbox/dcim/resource_netbox_dcim_region_test.go diff --git a/examples/resources/netbox_dcim_region/import.sh b/examples/resources/netbox_dcim_region/import.sh new file mode 100644 index 000000000..ff1b1eedb --- /dev/null +++ b/examples/resources/netbox_dcim_region/import.sh @@ -0,0 +1,2 @@ +# Regions can be imported by id +terraform import netbox_dcim_region.region_test 1 diff --git a/examples/resources/netbox_dcim_region/resource.tf b/examples/resources/netbox_dcim_region/resource.tf new file mode 100644 index 000000000..537c54814 --- /dev/null +++ b/examples/resources/netbox_dcim_region/resource.tf @@ -0,0 +1,82 @@ +resource "netbox_dcim_region" "region_test" { + name = "Test region" + slug = "Test region" + description = "region for testing" + + tag { + name = "tag1" + slug = "tag1" + } + + custom_field { + name = "cf_boolean" + type = "boolean" + value = "true" + } + + custom_field { + name = "cf_date" + type = "date" + value = "2020-12-25" + } + + custom_field { + name = "cf_text" + type = "text" + value = "some text" + } + + custom_field { + name = "cf_integer" + type = "integer" + value = "10" + } + + custom_field { + name = "cf_selection" + type = "select" + value = "1" + } + + custom_field { + name = "cf_url" + type = "url" + value = "https://github.com" + } + + custom_field { + name = "cf_multi_selection" + type = "multiselect" + value = jsonencode([ + "0", + "1" + ]) + } + + custom_field { + name = "cf_json" + type = "json" + value = jsonencode({ + stringvalue = "string" + boolvalue = false + dictionary = { + numbervalue = 5 + } + }) + } + + custom_field { + name = "cf_object" + type = "object" + value = 1 + } + + custom_field { + name = "cf_multi_object" + type = "multiobject" + value = jsonencode([ + 1, + 2 + ]) + } +} diff --git a/netbox/dcim/resource_netbox_dcim_region.go b/netbox/dcim/resource_netbox_dcim_region.go new file mode 100644 index 000000000..4e18670fa --- /dev/null +++ b/netbox/dcim/resource_netbox_dcim_region.go @@ -0,0 +1,277 @@ +package dcim + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + netboxclient "github.com/smutel/go-netbox/v3/netbox/client" + "github.com/smutel/go-netbox/v3/netbox/client/dcim" + "github.com/smutel/go-netbox/v3/netbox/models" + "github.com/smutel/terraform-provider-netbox/v6/netbox/internal/customfield" + "github.com/smutel/terraform-provider-netbox/v6/netbox/internal/requestmodifier" + "github.com/smutel/terraform-provider-netbox/v6/netbox/internal/tag" + "github.com/smutel/terraform-provider-netbox/v6/netbox/internal/util" +) + +func ResourceNetboxDcimRegion() *schema.Resource { + return &schema.Resource{ + Description: "Manage a region (dcim module) within Netbox.", + CreateContext: resourceNetboxDcimRegionCreate, + ReadContext: resourceNetboxDcimRegionRead, + UpdateContext: resourceNetboxDcimRegionUpdate, + DeleteContext: resourceNetboxDcimRegionDelete, + Exists: resourceNetboxDcimRegionExists, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "created": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this region (dcim module) was created.", + }, + "custom_field": &customfield.CustomFieldSchema, + "depth": { + Type: schema.TypeInt, + Computed: true, + Description: "Depth of this region (dcim module).", + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 200), + Description: "Description of this region (dcim module).", + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this region was last updated (dcim module).", + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), + Description: "The name of this region (dcim module).", + }, + "parent_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The ID of the parent for this region (dcim module).", + }, + "slug": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), + Description: "The slug of this site (dcim module).", + }, + "tag": &tag.TagSchema, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The link to this region (dcim module).", + }, + }, + } +} + +var regionRequiredFields = []string{ + "created", + "last_updated", + "name", + "slug", + "tags", +} + +func resourceNetboxDcimRegionCreate(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceCustomFields := d.Get("custom_field").(*schema.Set).List() + customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(nil, resourceCustomFields) + description := d.Get("description").(string) + name := d.Get("name").(string) + slug := d.Get("slug").(string) + tags := d.Get("tag").(*schema.Set).List() + parentID := int64(d.Get("parent_id").(int)) + + newResource := &models.WritableRegion{ + CustomFields: customFields, + Description: description, + Name: &name, + Slug: &slug, + Tags: tag.ConvertTagsToNestedTags(tags), + } + + if parentID != 0 { + newResource.Parent = &parentID + } + + resource := dcim.NewDcimRegionsCreateParams().WithData(newResource) + + resourceCreated, err := client.Dcim.DcimRegionsCreate(resource, nil) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(resourceCreated.Payload.ID, 10)) + + return resourceNetboxDcimRegionRead(ctx, d, m) +} + +func resourceNetboxDcimRegionRead(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceID := d.Id() + params := dcim.NewDcimRegionsListParams().WithID(&resourceID) + resources, err := client.Dcim.DcimRegionsList(params, nil) + if err != nil { + return diag.FromErr(err) + } + + if len(resources.Payload.Results) != 1 { + d.SetId("") + return nil + } + + resource := resources.Payload.Results[0] + if err = d.Set("created", resource.Created.String()); err != nil { + return diag.FromErr(err) + } + + resourceCustomFields := d.Get("custom_field").(*schema.Set).List() + customFields := customfield.UpdateCustomFieldsFromAPI(resourceCustomFields, resource.CustomFields) + + if err = d.Set("custom_field", customFields); err != nil { + return diag.FromErr(err) + } + if err = d.Set("description", resource.Description); err != nil { + return diag.FromErr(err) + } + if err = d.Set("depth", resource.Depth); err != nil { + return diag.FromErr(err) + } + if err = d.Set("last_updated", resource.LastUpdated.String()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("name", resource.Name); err != nil { + return diag.FromErr(err) + } + if err = d.Set("parent_id", util.GetNestedRegionParentID(resource.Parent)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("slug", resource.Slug); err != nil { + return diag.FromErr(err) + } + if err = d.Set("tag", tag.ConvertNestedTagsToTags(resource.Tags)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("url", resource.URL); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceNetboxDcimRegionUpdate(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.Errorf("Unable to convert ID into int64") + } + params := &models.WritableRegion{} + + modifiedFields := map[string]interface{}{} + if d.HasChange("custom_field") { + stateCustomFields, resourceCustomFields := d.GetChange("custom_field") + customFields := customfield.ConvertCustomFieldsFromTerraformToAPI( + stateCustomFields.(*schema.Set).List(), resourceCustomFields.(*schema.Set).List()) + params.CustomFields = &customFields + } + if d.HasChange("description") { + description := d.Get("description").(string) + params.Description = description + modifiedFields["description"] = description + } + if d.HasChange("name") { + name := d.Get("name").(string) + params.Name = &name + } + if d.HasChange("parent_id") { + parentID := int64(d.Get("parent_id").(int)) + params.Parent = &parentID + modifiedFields["parent"] = parentID + } + if d.HasChange("slug") { + slug := d.Get("slug").(string) + params.Slug = &slug + } + if d.HasChange("tag") { + tags := d.Get("tag").(*schema.Set).List() + params.Tags = tag.ConvertTagsToNestedTags(tags) + } + + resource := dcim.NewDcimRegionsPartialUpdateParams().WithData(params) + + resource.SetID(resourceID) + + _, err = client.Dcim.DcimRegionsPartialUpdate(resource, nil, requestmodifier.NewNetboxRequestModifier(modifiedFields, regionRequiredFields)) + if err != nil { + return diag.FromErr(err) + } + + return resourceNetboxDcimRegionRead(ctx, d, m) +} + +func resourceNetboxDcimRegionDelete(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceExists, err := resourceNetboxDcimRegionExists(d, m) + if err != nil { + return diag.FromErr(err) + } + + if !resourceExists { + return nil + } + + id, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.Errorf("Unable to convert ID into int64") + } + + resource := dcim.NewDcimRegionsDeleteParams().WithID(id) + if _, err := client.Dcim.DcimRegionsDelete(resource, nil); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceNetboxDcimRegionExists(d *schema.ResourceData, + m interface{}) (b bool, e error) { + client := m.(*netboxclient.NetBoxAPI) + resourceExist := false + + resourceID := d.Id() + params := dcim.NewDcimRegionsListParams().WithID(&resourceID) + resources, err := client.Dcim.DcimRegionsList(params, nil) + if err != nil { + return resourceExist, err + } + + for _, resource := range resources.Payload.Results { + if strconv.FormatInt(resource.ID, 10) == d.Id() { + resourceExist = true + } + } + + return resourceExist, nil +} diff --git a/netbox/dcim/resource_netbox_dcim_region_test.go b/netbox/dcim/resource_netbox_dcim_region_test.go new file mode 100644 index 000000000..864b7e920 --- /dev/null +++ b/netbox/dcim/resource_netbox_dcim_region_test.go @@ -0,0 +1,130 @@ +package dcim_test + +import ( + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/smutel/terraform-provider-netbox/v6/netbox/internal/util" +) + +const resourceNameNetboxDcimRegion = "netbox_dcim_region.test" + +func TestAccNetboxDcimRegionMinimal(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxDcimRegionConfig(nameSuffix, false, false, 0), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxDcimRegion), + ), + }, + { + ResourceName: resourceNameNetboxDcimRegion, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxDcimRegionFull(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + asn := int64(acctest.RandIntRange(1, 4294967295)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxDcimRegionConfig(nameSuffix, true, true, asn), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxDcimRegion), + ), + }, + { + ResourceName: resourceNameNetboxDcimRegion, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxDcimRegionMinimalFullMinimal(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + asn := int64(acctest.RandIntRange(1, 4294967295)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxDcimRegionConfig(nameSuffix, false, false, asn), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxDcimRegion), + ), + }, + { + Config: testAccCheckNetboxDcimRegionConfig(nameSuffix, true, true, asn), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxDcimRegion), + ), + }, + { + Config: testAccCheckNetboxDcimRegionConfig(nameSuffix, false, true, asn), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxDcimRegion), + ), + }, + { + Config: testAccCheckNetboxDcimRegionConfig(nameSuffix, false, false, asn), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxDcimRegion), + ), + }, + }, + }) +} + +func testAccCheckNetboxDcimRegionConfig(nameSuffix string, resourceFull, extraResources bool, asn int64) string { + template := ` + {{ if eq .extraresources "true" }} + resource "netbox_extras_tag" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_dcim_region" "test2" { + name = "test2-{{ .namesuffix }}" + slug = "test2-{{ .namesuffix }}" + } + {{ end }} + + resource "netbox_dcim_region" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + {{ if eq .resourcefull "true" }} + description = "Test region" + parent_id = netbox_dcim_region.test2.id + + tag { + name = netbox_extras_tag.test.name + slug = netbox_extras_tag.test.slug + } + {{ end }} + } + ` + data := map[string]string{ + "namesuffix": nameSuffix, + "resourcefull": strconv.FormatBool(resourceFull), + "extraresources": strconv.FormatBool(extraResources), + "asn": strconv.FormatInt(asn, 10), + } + return util.RenderTemplate(template, data) +} diff --git a/netbox/internal/util/nested.go b/netbox/internal/util/nested.go index 6494d73e7..454e15653 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -195,3 +195,11 @@ func GetRackOuterUnit(nested *models.RackOuterUnit) *string { return nested.Value } + +func GetNestedRegionParentID(nested *models.NestedRegion) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} diff --git a/netbox/provider.go b/netbox/provider.go index 757e7e0f2..f05b08783 100644 --- a/netbox/provider.go +++ b/netbox/provider.go @@ -170,6 +170,7 @@ func Provider() *schema.Provider { "netbox_dcim_platform": dcim.ResourceNetboxDcimPlatform(), "netbox_dcim_rack": dcim.ResourceNetboxDcimRack(), "netbox_dcim_rack_role": dcim.ResourceNetboxDcimRackRole(), + "netbox_dcim_region": dcim.ResourceNetboxDcimRegion(), "netbox_dcim_site": dcim.ResourceNetboxDcimSite(), "netbox_extras_custom_field": extras.ResourceNetboxExtrasCustomField(), "netbox_extras_tag": extras.ResourceNetboxExtrasTag(),