From 67e762893a894efe9566959c7c386b462e53b589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 30 Oct 2022 19:58:54 +0100 Subject: [PATCH] feat: Add scope field to VLAN group resource Fixes: 193 --- examples/main.tf | 85 ++++++- .../netbox_ipam_vlan_group/import.sh | 2 + .../netbox_ipam_vlan_group/resource.tf | 80 +++++++ .../ipam/resource_netbox_ipam_vlan_group.go | 215 +++++++++++++++--- .../resource_netbox_ipam_vlan_group_test.go | 134 +++++++++++ 5 files changed, 482 insertions(+), 34 deletions(-) create mode 100644 examples/resources/netbox_ipam_vlan_group/import.sh create mode 100644 netbox/ipam/resource_netbox_ipam_vlan_group_test.go diff --git a/examples/main.tf b/examples/main.tf index 55184672f..2cf6525ec 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -114,17 +114,92 @@ data "netbox_dcim_site" "site_test" { } resource "netbox_ipam_vlan_group" "vlan_group_test" { - name = "Test_VlanGroup" - slug = "Test_VlanGroup" + name = "TestVlanGroup" + slug = "TestVlanGroup" + + max_vid = 4094 + min_vid = 1 + + scope { + id = 1 + type = "dcim.site" + } tag { name = "tag1" slug = "tag1" } - tag { - name = "tag2" - slug = "tag2" + 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/examples/resources/netbox_ipam_vlan_group/import.sh b/examples/resources/netbox_ipam_vlan_group/import.sh new file mode 100644 index 000000000..bcf840261 --- /dev/null +++ b/examples/resources/netbox_ipam_vlan_group/import.sh @@ -0,0 +1,2 @@ +# VLAN groups can be imported by id +terraform import netbox_ipam_vlan_group.vlan_group_test 1 diff --git a/examples/resources/netbox_ipam_vlan_group/resource.tf b/examples/resources/netbox_ipam_vlan_group/resource.tf index ffec0b9ed..1815aff8d 100644 --- a/examples/resources/netbox_ipam_vlan_group/resource.tf +++ b/examples/resources/netbox_ipam_vlan_group/resource.tf @@ -1,9 +1,89 @@ resource "netbox_ipam_vlan_group" "vlan_group_test" { name = "TestVlanGroup" slug = "TestVlanGroup" + description = "Vlan group created by terraform" + max_vid = 4094 + min_vid = 1 + + scope { + id = 1 + type = "dcim.site" + } 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/ipam/resource_netbox_ipam_vlan_group.go b/netbox/ipam/resource_netbox_ipam_vlan_group.go index cdbd65e87..36d292f4a 100644 --- a/netbox/ipam/resource_netbox_ipam_vlan_group.go +++ b/netbox/ipam/resource_netbox_ipam_vlan_group.go @@ -11,6 +11,8 @@ import ( netboxclient "github.com/smutel/go-netbox/v3/netbox/client" "github.com/smutel/go-netbox/v3/netbox/client/ipam" "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" ) @@ -33,12 +35,64 @@ func ResourceNetboxIpamVlanGroup() *schema.Resource { Computed: true, Description: "The content type of this vlan group (ipam module).", }, + "created": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this resource was created.", + }, + "custom_field": &customfield.CustomFieldSchema, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 100), + Description: "The description of this vlan group (ipam module).", + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this resource was last updated.", + }, + "max_vid": { + Type: schema.TypeInt, + Optional: true, + Default: 4094, + ValidateFunc: validation.IntBetween(1, 4094), + Description: "Highest permissible ID of a child vlan (ipam module).", + }, + "min_vid": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + ValidateFunc: validation.IntBetween(1, 4094), + Description: "Lowest permissible ID of a child vlan (ipam module).", + }, "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringLenBetween(1, 50), Description: "The name for this vlan group (ipam module).", }, + "scope": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Required: true, + Description: "ID of the scope object for this vlan group (ipam module).", + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"dcim.location", "dcim.rack", "dcim.region", "dcim.site", "dcim.sitegroup", "virtualization.cluster", "virtualization.clustergroup"}, false), + Description: "Type of the scope object. Must me one of \"dcim.location\", \"dcim.rack\", \"dcim.region\", \"dcim.site\", \"dcim.sitegroup\", \"virtualization.cluster\", \"virtualization.clustergroup\".", + }, + }, + }, + Description: "Scope of this vlan group (ipam module).", + }, "slug": { Type: schema.TypeString, Required: true, @@ -48,22 +102,54 @@ func ResourceNetboxIpamVlanGroup() *schema.Resource { Description: "The slug for this vlan group (ipam module).", }, "tag": &tag.TagSchema, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The link to this vlan group (ipam module).", + }, + "vlan_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The number of vlans assigned to this vlan group (ipam module).", + }, }, } } +var vlanGroupRequiredFields = []string{ + "created", + "last_updated", + "name", + "slug", + "tags", +} + func resourceNetboxIpamVlanGroupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { client := m.(*netboxclient.NetBoxAPI) groupName := d.Get("name").(string) groupSlug := d.Get("slug").(string) + resourceCustomFields := d.Get("custom_field").(*schema.Set).List() + customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(nil, resourceCustomFields) tags := d.Get("tag").(*schema.Set).List() newResource := &models.VLANGroup{ - Name: &groupName, - Slug: &groupSlug, - Tags: tag.ConvertTagsToNestedTags(tags), + CustomFields: customFields, + Description: d.Get("description").(string), + MaxVid: int64(d.Get("max_vid").(int)), + MinVid: int64(d.Get("min_vid").(int)), + Name: &groupName, + Slug: &groupSlug, + Tags: tag.ConvertTagsToNestedTags(tags), + } + + if scopes := d.Get("scope").(*schema.Set).List(); len(scopes) == 1 { + scopeIntf := scopes[0].(map[string]interface{}) + scopeID := int64(scopeIntf["id"].(int)) + scopeType := scopeIntf["type"].(string) + newResource.ScopeID = &scopeID + newResource.ScopeType = &scopeType } resource := ipam.NewIpamVlanGroupsCreateParams().WithData(newResource) @@ -88,29 +174,66 @@ func resourceNetboxIpamVlanGroupRead(ctx context.Context, d *schema.ResourceData return diag.FromErr(err) } - for _, resource := range resources.Payload.Results { - if strconv.FormatInt(resource.ID, 10) == d.Id() { - if err = d.Set("content_type", util.ConvertURIContentType(resource.URL)); err != nil { - return diag.FromErr(err) - } + if len(resources.Payload.Results) != 1 { + d.SetId("") + return nil + } - if err = d.Set("name", resource.Name); err != nil { - return diag.FromErr(err) - } + resource := resources.Payload.Results[0] - if err = d.Set("slug", resource.Slug); err != nil { - return diag.FromErr(err) - } + if err = d.Set("content_type", util.ConvertURIContentType(resource.URL)); err != nil { + return diag.FromErr(err) + } + + 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("tag", tag.ConvertNestedTagsToTags(resource.Tags)); 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("max_vid", resource.MaxVid); err != nil { + return diag.FromErr(err) + } + if err = d.Set("min_vid", resource.MinVid); err != nil { + return diag.FromErr(err) + } + if err = d.Set("name", resource.Name); err != nil { + return diag.FromErr(err) + } - return nil + var scopes []map[string]interface{} + if resource.Scope != nil { + scopes = []map[string]interface{}{ + { + "id": resource.ScopeID, + "type": resource.ScopeType, + }, } } + if err = d.Set("scope", scopes); 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("vlan_count", resource.VlanCount); err != nil { + return diag.FromErr(err) + } - d.SetId("") return nil } @@ -118,16 +241,50 @@ func resourceNetboxIpamVlanGroupUpdate(ctx context.Context, d *schema.ResourceDa m interface{}) diag.Diagnostics { client := m.(*netboxclient.NetBoxAPI) params := &models.VLANGroup{} + modifiedFields := map[string]interface{}{} - // Required parameters - name := d.Get("name").(string) - params.Name = &name - - slug := d.Get("slug").(string) - params.Slug = &slug - - tags := d.Get("tag").(*schema.Set).List() - params.Tags = tag.ConvertTagsToNestedTags(tags) + 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("max_vid") { + params.MaxVid = int64(d.Get("max_vid").(int)) + } + if d.HasChange("min_vid") { + params.MinVid = int64(d.Get("min_vid").(int)) + } + if d.HasChange("name") { + name := d.Get("name").(string) + params.Name = &name + } + if d.HasChange("scope") { + if scopes := d.Get("scope").(*schema.Set).List(); len(scopes) == 1 { + scopeIntf := scopes[0].(map[string]interface{}) + scopeID := int64(scopeIntf["id"].(int)) + scopeType := scopeIntf["type"].(string) + params.ScopeID = &scopeID + params.ScopeType = &scopeType + modifiedFields["scope_id"] = scopeID + modifiedFields["scope_type"] = scopeType + } else { + modifiedFields["scope_id"] = nil + modifiedFields["scope_type"] = nil + } + } + 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 := ipam.NewIpamVlanGroupsPartialUpdateParams().WithData( params) @@ -139,7 +296,7 @@ func resourceNetboxIpamVlanGroupUpdate(ctx context.Context, d *schema.ResourceDa resource.SetID(resourceID) - _, err = client.Ipam.IpamVlanGroupsPartialUpdate(resource, nil) + _, err = client.Ipam.IpamVlanGroupsPartialUpdate(resource, nil, requestmodifier.NewNetboxRequestModifier(modifiedFields, vlanGroupRequiredFields)) if err != nil { return diag.FromErr(err) } diff --git a/netbox/ipam/resource_netbox_ipam_vlan_group_test.go b/netbox/ipam/resource_netbox_ipam_vlan_group_test.go new file mode 100644 index 000000000..2c97434b4 --- /dev/null +++ b/netbox/ipam/resource_netbox_ipam_vlan_group_test.go @@ -0,0 +1,134 @@ +package ipam_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 resourceNameVlanGroup = "netbox_ipam_vlan_group.test" + +func TestAccNetboxIpamVlanGroupMinimal(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: testAccCheckNetboxIpamVlanGroupConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameVlanGroup), + ), + }, + { + ResourceName: resourceNameVlanGroup, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxIpamVlanGroupFull(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: testAccCheckNetboxIpamVlanGroupConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameVlanGroup), + ), + }, + { + ResourceName: resourceNameVlanGroup, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxIpamVlanGroupMinimalFullMinimal(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: testAccCheckNetboxIpamVlanGroupConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameVlanGroup), + ), + }, + { + Config: testAccCheckNetboxIpamVlanGroupConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameVlanGroup), + ), + }, + // This step is necessary. Otherwise deleting site deletes the vlan groups assigned to site. + { + Config: testAccCheckNetboxIpamVlanGroupConfig(nameSuffix, false, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameVlanGroup), + ), + }, + { + Config: testAccCheckNetboxIpamVlanGroupConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameVlanGroup), + ), + }, + }, + }) +} + +func testAccCheckNetboxIpamVlanGroupConfig(nameSuffix string, resourceFull, extraResources bool) string { + const template = ` + {{ if eq .extraresources "true" }} + resource "netbox_dcim_site" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_extras_tag" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + {{ end }} + + resource "netbox_ipam_vlan_group" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + {{ if eq .resourcefull "true" }} + description = "Test Vlan group" + max_vid = 2369 + min_vid = 135 + + scope { + id = netbox_dcim_site.test.id + type = "dcim.site" + } + + tag { + name = netbox_extras_tag.test.name + slug = netbox_extras_tag.test.slug + } + {{ end }} + } + ` + data := map[string]string{ + "namesuffix": nameSuffix, + "extraresources": strconv.FormatBool(extraResources), + "resourcefull": strconv.FormatBool(resourceFull), + } + return util.RenderTemplate(template, data) +}