From a292478f10eefe9534e58a54fea6d09c69e001e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 30 Oct 2022 21:07:36 +0100 Subject: [PATCH] feat: Add netbox_virtualization_cluster resource --- .../netbox_virtualization_cluster/resource.tf | 89 +++++ netbox/internal/util/nested.go | 16 + netbox/provider.go | 1 + .../resource_netbox_virtualization_cluster.go | 326 ++++++++++++++++++ ...urce_netbox_virtualization_cluster_test.go | 147 ++++++++ ...etbox_virtualization_vm_primary_ip_test.go | 23 +- 6 files changed, 588 insertions(+), 14 deletions(-) create mode 100644 examples/resources/netbox_virtualization_cluster/resource.tf create mode 100644 netbox/virtualization/resource_netbox_virtualization_cluster.go create mode 100644 netbox/virtualization/resource_netbox_virtualization_cluster_test.go diff --git a/examples/resources/netbox_virtualization_cluster/resource.tf b/examples/resources/netbox_virtualization_cluster/resource.tf new file mode 100644 index 000000000..2123c86e9 --- /dev/null +++ b/examples/resources/netbox_virtualization_cluster/resource.tf @@ -0,0 +1,89 @@ +resource "netbox_virtualization_cluster" "cluster_test" { + name = "Test cluster" + type_id = netbox_virtualization_cluster_type.test.id + + comments = <<-EOT + Test cluster + EOT + + group_id = netbox_virtualization_cluster_group.test.id + site_id = netbox_dcim_site.site_test.id + tenant_id = netbox_tenancy_tenant.tenant_test.id + + 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/internal/util/nested.go b/netbox/internal/util/nested.go index 9e395c58d..7a7669859 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -12,6 +12,14 @@ func GetNestedIPAddressAddress(nested *models.NestedIPAddress) *string { return nested.Address } +func GetNestedClusterGroupID(nested *models.NestedClusterGroup) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} + func GetNestedManufacturerID(nested *models.NestedManufacturer) *int64 { if nested == nil { return nil @@ -36,6 +44,14 @@ func GetNestedRegionID(nested *models.NestedRegion) *int64 { return &nested.ID } +func GetNestedSiteID(nested *models.NestedSite) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} + func GetNestedSiteGroupID(nested *models.NestedSiteGroup) *int64 { if nested == nil { return nil diff --git a/netbox/provider.go b/netbox/provider.go index c03697943..30a226e2e 100644 --- a/netbox/provider.go +++ b/netbox/provider.go @@ -183,6 +183,7 @@ func Provider() *schema.Provider { "netbox_tenancy_contact_role": tenancy.ResourceNetboxTenancyContactRole(), "netbox_tenancy_tenant": tenancy.ResourceNetboxTenancyTenant(), "netbox_tenancy_tenant_group": tenancy.ResourceNetboxTenancyTenantGroup(), + "netbox_virtualization_cluster": virtualization.ResourceNetboxVirtualizationCluster(), "netbox_virtualization_cluster_group": virtualization.ResourceNetboxVirtualizationClusterGroup(), "netbox_virtualization_cluster_type": virtualization.ResourceNetboxVirtualizationClusterType(), "netbox_virtualization_interface": virtualization.ResourceNetboxVirtualizationInterface(), diff --git a/netbox/virtualization/resource_netbox_virtualization_cluster.go b/netbox/virtualization/resource_netbox_virtualization_cluster.go new file mode 100644 index 000000000..79eb647a3 --- /dev/null +++ b/netbox/virtualization/resource_netbox_virtualization_cluster.go @@ -0,0 +1,326 @@ +package virtualization + +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/virtualization" + "github.com/smutel/go-netbox/v3/netbox/models" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/customfield" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/requestmodifier" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/tag" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/util" +) + +func ResourceNetboxVirtualizationCluster() *schema.Resource { + return &schema.Resource{ + Description: "Manage a tag (extra module) within Netbox.", + CreateContext: resourceNetboxVirtualizationClusterCreate, + ReadContext: resourceNetboxVirtualizationClusterRead, + UpdateContext: resourceNetboxVirtualizationClusterUpdate, + DeleteContext: resourceNetboxVirtualizationClusterDelete, + Exists: resourceNetboxVirtualizationClusterExists, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "content_type": { + Type: schema.TypeString, + Computed: true, + Description: "The content type of this cluster (virtualization module).", + }, + "comments": { + Type: schema.TypeString, + Optional: true, + StateFunc: util.TrimString, + Description: "Comments for this cluster (virtualization module).", + }, + "created": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this cluster was created.", + }, + "custom_field": &customfield.CustomFieldSchema, + "device_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of devices in this cluster.", + }, + "group_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The cluster group of this cluster.", + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this cluster was last updated.", + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), + Description: "The name of this cluster (virtualization module).", + }, + "site_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The site of this cluster.", + }, + "tag": &tag.TagSchema, + "tenant_id": { + Type: schema.TypeInt, + Optional: true, + Default: nil, + Description: "ID of the tenant where this cluster is attached.", + }, + "type_id": { + Type: schema.TypeInt, + Required: true, + Default: nil, + Description: "Type of this cluster.", + }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The link to this cluster (virtualization module).", + }, + "virtualmachine_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of virtual machines in this cluster.", + }, + }, + } +} + +var clusterRequiredFields = []string{ + "created", + "last_updated", + "name", + "type", + "tags", +} + +func resourceNetboxVirtualizationClusterCreate(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) + groupID := int64(d.Get("group_id").(int)) + name := d.Get("name").(string) + typeID := int64(d.Get("type_id").(int)) + siteID := int64(d.Get("site_id").(int)) + tenantID := int64(d.Get("tenant_id").(int)) + tags := d.Get("tag").(*schema.Set).List() + + newResource := &models.WritableCluster{ + Comments: d.Get("comments").(string), + CustomFields: customFields, + Name: &name, + Tags: tag.ConvertTagsToNestedTags(tags), + Type: &typeID, + } + + if groupID != 0 { + newResource.Group = &groupID + } + + if siteID != 0 { + newResource.Site = &siteID + } + if tenantID != 0 { + newResource.Tenant = &tenantID + } + + resource := virtualization.NewVirtualizationClustersCreateParams().WithData(newResource) + + resourceCreated, err := client.Virtualization.VirtualizationClustersCreate(resource, nil) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(resourceCreated.Payload.ID, 10)) + + return resourceNetboxVirtualizationClusterRead(ctx, d, m) +} + +func resourceNetboxVirtualizationClusterRead(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceID := d.Id() + params := virtualization.NewVirtualizationClustersListParams().WithID(&resourceID) + resources, err := client.Virtualization.VirtualizationClustersList(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("comments", resource.Comments); 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("device_count", resource.DeviceCount); err != nil { + return diag.FromErr(err) + } + if err = d.Set("group_id", util.GetNestedClusterGroupID(resource.Group)); 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("site_id", util.GetNestedSiteID(resource.Site)); 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("tenant_id", util.GetNestedTenantID(resource.Tenant)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("type_id", resource.Type.ID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("url", resource.URL); err != nil { + return diag.FromErr(err) + } + if err = d.Set("virtualmachine_count", resource.VirtualmachineCount); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceNetboxVirtualizationClusterUpdate(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + modifiedFields := make(map[string]interface{}) + + resourceID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.Errorf("Unable to convert ID into int64") + } + params := &models.WritableCluster{} + + if d.HasChange("comments") { + params.Comments = d.Get("comments").(string) + modifiedFields["comments"] = params.Comments + } + 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("group_id") { + groupID := int64(d.Get("group_id").(int)) + params.Group = &groupID + modifiedFields["group"] = groupID + } + if d.HasChange("name") { + name := d.Get("name").(string) + params.Name = &name + } + if d.HasChange("site_id") { + siteID := int64(d.Get("site_id").(int)) + params.Site = &siteID + modifiedFields["site"] = siteID + } + if d.HasChange("tag") { + tags := d.Get("tag").(*schema.Set).List() + params.Tags = tag.ConvertTagsToNestedTags(tags) + } + if d.HasChange("tenant_id") { + tenantID := int64(d.Get("tenant_id").(int)) + params.Tenant = &tenantID + modifiedFields["tenant"] = tenantID + } + if d.HasChange("type_id") { + typeID := int64(d.Get("type_id").(int)) + params.Type = &typeID + } + + resource := virtualization.NewVirtualizationClustersPartialUpdateParams().WithData(params) + + resource.SetID(resourceID) + + _, err = client.Virtualization.VirtualizationClustersPartialUpdate(resource, nil, requestmodifier.NewNetboxRequestModifier(modifiedFields, clusterRequiredFields)) + if err != nil { + return diag.FromErr(err) + } + + return resourceNetboxVirtualizationClusterRead(ctx, d, m) +} + +func resourceNetboxVirtualizationClusterDelete(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceExists, err := resourceNetboxVirtualizationClusterExists(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 := virtualization.NewVirtualizationClustersDeleteParams().WithID(id) + if _, err := client.Virtualization.VirtualizationClustersDelete(resource, nil); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceNetboxVirtualizationClusterExists(d *schema.ResourceData, + m interface{}) (b bool, e error) { + client := m.(*netboxclient.NetBoxAPI) + resourceExist := false + + resourceID := d.Id() + params := virtualization.NewVirtualizationClustersListParams().WithID(&resourceID) + resources, err := client.Virtualization.VirtualizationClustersList(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/virtualization/resource_netbox_virtualization_cluster_test.go b/netbox/virtualization/resource_netbox_virtualization_cluster_test.go new file mode 100644 index 000000000..2d861ee9d --- /dev/null +++ b/netbox/virtualization/resource_netbox_virtualization_cluster_test.go @@ -0,0 +1,147 @@ +package virtualization_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/v4/netbox/internal/util" +) + +const resourceNameNetboxVirtualizationCluster = "netbox_virtualization_cluster.test" + +func TestAccNetboxVirtualizationClusterMinimal(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: testAccCheckNetboxVirtualizationClusterConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationCluster), + ), + }, + { + ResourceName: resourceNameNetboxVirtualizationCluster, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxVirtualizationClusterFull(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: testAccCheckNetboxVirtualizationClusterConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationCluster), + ), + }, + { + ResourceName: resourceNameNetboxVirtualizationCluster, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxVirtualizationClusterMinimalFullMinimal(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: testAccCheckNetboxVirtualizationClusterConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationCluster), + ), + }, + { + Config: testAccCheckNetboxVirtualizationClusterConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationCluster), + ), + }, + { + Config: testAccCheckNetboxVirtualizationClusterConfig(nameSuffix, false, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationCluster), + ), + }, + { + Config: testAccCheckNetboxVirtualizationClusterConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationCluster), + ), + }, + }, + }) +} + +func testAccCheckNetboxVirtualizationClusterConfig(nameSuffix string, resourceFull, extraResources bool) string { + template := ` + resource "netbox_virtualization_cluster_type" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + {{ if eq .extraresources "true" }} + resource "netbox_dcim_site" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_virtualization_cluster_group" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_tenancy_tenant" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_extras_tag" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + {{ end }} + + resource "netbox_virtualization_cluster" "test" { + name = "test-{{ .namesuffix }}" + type_id = netbox_virtualization_cluster_type.test.id + {{ if eq .resourcefull "true" }} + group_id = netbox_virtualization_cluster_group.test.id + site_id = netbox_dcim_site.test.id + tenant_id = netbox_tenancy_tenant.test.id + + comments = <<-EOT + Test cluster + EOT + + 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), + } + return util.RenderTemplate(template, data) +} diff --git a/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip_test.go b/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip_test.go index f5f119171..9135eb9c6 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip_test.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip_test.go @@ -115,24 +115,19 @@ func TestAccNetboxVirtualizationVMPrimaryIPLegacy(t *testing.T) { func testAccCheckNetboxVirtualizationVMPrimaryIPConfig(nameSuffix string, resourceFull, extraResources bool, ipnum int64, legacy bool) string { template := ` - #resource "netbox_virtualization_cluster_type" "test" { - # name = "test-{{ .namesuffix }}" - # slug = "test-{{ .namesuffix }}" - #} - - #resource "netbox_virtualization_cluster" "test" { - # name = "test-{{ .namesuffix }}" - # type_id = netbox_virtualization_cluster_type.test.id - #} - - data "netbox_virtualization_cluster" "cluster_test" { - name = "test" + resource "netbox_virtualization_cluster_type" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_virtualization_cluster" "test" { + name = "test-{{ .namesuffix }}" + type_id = netbox_virtualization_cluster_type.test.id } resource "netbox_virtualization_vm" "test" { name = "test-{{ .namesuffix }}" - #cluster_id = netbox_virtualization_cluster.test.id - cluster_id = data.netbox_virtualization_cluster.cluster_test.id + cluster_id = netbox_virtualization_cluster.test.id } {{ if eq .extraresources "true" }}