From 6cd4162a586489aab650f9b45a684e1e48e60374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sat, 4 Mar 2023 20:36:47 +0100 Subject: [PATCH 01/13] fix: Export files not working with Netbox 3.3 fixes: #187 --- .../netbox_virtualization_cluster types.csv | 2 -- .../v3.2/netbox_virtualization_clusters.csv | 2 -- .../netbox_dcim_device roles.csv | 0 .../{v3.2 => v3.3}/netbox_dcim_platforms.csv | 0 .../{v3.2 => v3.3}/netbox_dcim_sites.csv | 0 .../netbox_extras_custom fields.csv | 22 +++++++++---------- .../{v3.2 => v3.3}/netbox_extras_tags.csv | 0 .../{v3.2 => v3.3}/netbox_ipam_RIRs.csv | 0 .../{v3.2 => v3.3}/netbox_ipam_roles.csv | 0 .../netbox_virtualization_cluster types.csv | 2 ++ .../v3.3/netbox_virtualization_clusters.csv | 2 ++ examples/main.tf | 2 +- .../resource_netbox_virtualization_vm_test.go | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 examples/exports/v3.2/netbox_virtualization_cluster types.csv delete mode 100644 examples/exports/v3.2/netbox_virtualization_clusters.csv rename examples/exports/{v3.2 => v3.3}/netbox_dcim_device roles.csv (100%) rename examples/exports/{v3.2 => v3.3}/netbox_dcim_platforms.csv (100%) rename examples/exports/{v3.2 => v3.3}/netbox_dcim_sites.csv (100%) rename examples/exports/{v3.2 => v3.3}/netbox_extras_custom fields.csv (79%) rename examples/exports/{v3.2 => v3.3}/netbox_extras_tags.csv (100%) rename examples/exports/{v3.2 => v3.3}/netbox_ipam_RIRs.csv (100%) rename examples/exports/{v3.2 => v3.3}/netbox_ipam_roles.csv (100%) create mode 100644 examples/exports/v3.3/netbox_virtualization_cluster types.csv create mode 100644 examples/exports/v3.3/netbox_virtualization_clusters.csv diff --git a/examples/exports/v3.2/netbox_virtualization_cluster types.csv b/examples/exports/v3.2/netbox_virtualization_cluster types.csv deleted file mode 100644 index 62b980014..000000000 --- a/examples/exports/v3.2/netbox_virtualization_cluster types.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,description,slug -test,"Test Cluster Type",test diff --git a/examples/exports/v3.2/netbox_virtualization_clusters.csv b/examples/exports/v3.2/netbox_virtualization_clusters.csv deleted file mode 100644 index 7f4be1c11..000000000 --- a/examples/exports/v3.2/netbox_virtualization_clusters.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,type -test,test diff --git a/examples/exports/v3.2/netbox_dcim_device roles.csv b/examples/exports/v3.3/netbox_dcim_device roles.csv similarity index 100% rename from examples/exports/v3.2/netbox_dcim_device roles.csv rename to examples/exports/v3.3/netbox_dcim_device roles.csv diff --git a/examples/exports/v3.2/netbox_dcim_platforms.csv b/examples/exports/v3.3/netbox_dcim_platforms.csv similarity index 100% rename from examples/exports/v3.2/netbox_dcim_platforms.csv rename to examples/exports/v3.3/netbox_dcim_platforms.csv diff --git a/examples/exports/v3.2/netbox_dcim_sites.csv b/examples/exports/v3.3/netbox_dcim_sites.csv similarity index 100% rename from examples/exports/v3.2/netbox_dcim_sites.csv rename to examples/exports/v3.3/netbox_dcim_sites.csv diff --git a/examples/exports/v3.2/netbox_extras_custom fields.csv b/examples/exports/v3.3/netbox_extras_custom fields.csv similarity index 79% rename from examples/exports/v3.2/netbox_extras_custom fields.csv rename to examples/exports/v3.3/netbox_extras_custom fields.csv index e084ade2b..c49360b24 100644 --- a/examples/exports/v3.2/netbox_extras_custom fields.csv +++ b/examples/exports/v3.3/netbox_extras_custom fields.csv @@ -1,11 +1,11 @@ -name,content_types,label,type,required,description,weight,default,filter_logic,choices,object_type -cf_boolean,"virtualization.virtualmachine,tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,ipam.vlan,ipam.service,ipam.iprange","Boolean field",boolean,False,,100,,loose,, -cf_date,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,date,False,,100,,loose,, -cf_integer,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,integer,False,,100,,loose,, -cf_multi_selection,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,multiselect,False,,100,,loose,"0,1", -cf_selection,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,select,False,,100,,loose,"0,1", -cf_text,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,text,False,,100,,loose,, -cf_url,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,url,False,,100,,loose,, -cf_json,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,json,False,,100,,loose,, -cf_multi_object,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,multiobject,False,,100,,loose,,dcim.platform -cf_object,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,multiobject,False,,100,,loose,,dcim.platform +name,content_types,label,type,required,description,weight,default,filter_logic,choices,object_type,ui_visibility +cf_boolean,"virtualization.virtualmachine,tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,ipam.vlan,ipam.service,ipam.iprange","Boolean field",boolean,False,,100,,loose,,,read-write +cf_date,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,date,False,,100,,loose,,,read-write +cf_integer,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,integer,False,,100,,loose,,,read-write +cf_multi_selection,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,multiselect,False,,100,,loose,"0,1",,read-write +cf_selection,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,select,False,,100,,loose,"0,1",,read-write +cf_text,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,text,False,,100,,loose,,,read-write +cf_url,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,url,False,,100,,loose,,,read-write +cf_json,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,json,False,,100,,loose,,,read-write +cf_multi_object,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,multiobject,False,,100,,loose,,dcim.platform,read-write +cf_object,"tenancy.tenant,tenancy.contactrole,tenancy.contactgroup,ipam.aggregate,ipam.ipaddress,ipam.prefix,tenancy.contact,virtualization.virtualmachine,ipam.vlan,ipam.service,ipam.iprange",,multiobject,False,,100,,loose,,dcim.platform,read-write diff --git a/examples/exports/v3.2/netbox_extras_tags.csv b/examples/exports/v3.3/netbox_extras_tags.csv similarity index 100% rename from examples/exports/v3.2/netbox_extras_tags.csv rename to examples/exports/v3.3/netbox_extras_tags.csv diff --git a/examples/exports/v3.2/netbox_ipam_RIRs.csv b/examples/exports/v3.3/netbox_ipam_RIRs.csv similarity index 100% rename from examples/exports/v3.2/netbox_ipam_RIRs.csv rename to examples/exports/v3.3/netbox_ipam_RIRs.csv diff --git a/examples/exports/v3.2/netbox_ipam_roles.csv b/examples/exports/v3.3/netbox_ipam_roles.csv similarity index 100% rename from examples/exports/v3.2/netbox_ipam_roles.csv rename to examples/exports/v3.3/netbox_ipam_roles.csv diff --git a/examples/exports/v3.3/netbox_virtualization_cluster types.csv b/examples/exports/v3.3/netbox_virtualization_cluster types.csv new file mode 100644 index 000000000..c638b896d --- /dev/null +++ b/examples/exports/v3.3/netbox_virtualization_cluster types.csv @@ -0,0 +1,2 @@ +name,description,slug +"Test Cluster Type","Test Cluster Type",test_cluster_type diff --git a/examples/exports/v3.3/netbox_virtualization_clusters.csv b/examples/exports/v3.3/netbox_virtualization_clusters.csv new file mode 100644 index 000000000..8280f8c19 --- /dev/null +++ b/examples/exports/v3.3/netbox_virtualization_clusters.csv @@ -0,0 +1,2 @@ +name,type,status +"Test Cluster","Test Cluster Type",active diff --git a/examples/main.tf b/examples/main.tf index d530104eb..55184672f 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -525,7 +525,7 @@ resource "netbox_ipam_ip_addresses" "dynamic_ip_from_ip_range" { } data "netbox_virtualization_cluster" "cluster_test" { - name = "test" + name = "Test Cluster" } data "netbox_dcim_platform" "platform_test" { diff --git a/netbox/virtualization/resource_netbox_virtualization_vm_test.go b/netbox/virtualization/resource_netbox_virtualization_vm_test.go index df2fb2f3d..6a47ac43d 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm_test.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm_test.go @@ -102,7 +102,7 @@ func testAccCheckNetboxVirtualizationVMConfig(nameSuffix string, resourceFull, e # type_id = netbox_virtualization_cluster_type.test.id #} data "netbox_virtualization_cluster" "cluster_test" { - name = "test" + name = "Test Cluster" } {{ if eq .extraresources "true" }} From c17026d06be926075dc833876606a33417926e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 5 Mar 2023 06:32:26 +0100 Subject: [PATCH 02/13] fix: Import does not work for virtualization_vm_primary_ip fixes: #188 --- .../resources/netbox_virtualization_vm_primary_ip/import.sh | 2 ++ .../resource_netbox_virtualization_vm_primary_ip.go | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 examples/resources/netbox_virtualization_vm_primary_ip/import.sh diff --git a/examples/resources/netbox_virtualization_vm_primary_ip/import.sh b/examples/resources/netbox_virtualization_vm_primary_ip/import.sh new file mode 100644 index 000000000..da49f1b0b --- /dev/null +++ b/examples/resources/netbox_virtualization_vm_primary_ip/import.sh @@ -0,0 +1,2 @@ +# Virtual machine primary IP assignments can be imported by virtual machine id +terraform import netbox_virtualization_vm_primary_ip.name 1 diff --git a/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip.go b/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip.go index 6cd9a83f9..2fdd982eb 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm_primary_ip.go @@ -249,6 +249,9 @@ func resourceNetboxVirtualizationVMPrimaryIPExists(d *schema.ResourceData, resourceID := int64(d.Get("virtualmachine_id").(int)) resourceIDString := strconv.FormatInt(resourceID, 10) + if resourceIDString == "0" { + resourceIDString = d.Id() + } params := virtualization.NewVirtualizationVirtualMachinesListParams().WithID( &resourceIDString) resources, err := client.Virtualization.VirtualizationVirtualMachinesList( @@ -258,7 +261,7 @@ func resourceNetboxVirtualizationVMPrimaryIPExists(d *schema.ResourceData, } for _, resource := range resources.Payload.Results { - if resource.ID == resourceID { + if strconv.FormatInt(resource.ID, 10) == resourceIDString { resourceExist = true } } From dca1f546ff1c064caab1ff96fa6f4d7dbd06c4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 5 Mar 2023 06:31:33 +0100 Subject: [PATCH 03/13] fix: Typo for tags field in ipam_rir resource --- examples/resources/netbox_ipam_rir/import.sh | 2 ++ netbox/ipam/resource_netbox_ipam_rir.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 examples/resources/netbox_ipam_rir/import.sh diff --git a/examples/resources/netbox_ipam_rir/import.sh b/examples/resources/netbox_ipam_rir/import.sh new file mode 100644 index 000000000..741dc9947 --- /dev/null +++ b/examples/resources/netbox_ipam_rir/import.sh @@ -0,0 +1,2 @@ +# RIRs can be imported by id +terraform import netbox_ipam_rir.rir_test 1 diff --git a/netbox/ipam/resource_netbox_ipam_rir.go b/netbox/ipam/resource_netbox_ipam_rir.go index 6abafe12a..49dfa0e7d 100644 --- a/netbox/ipam/resource_netbox_ipam_rir.go +++ b/netbox/ipam/resource_netbox_ipam_rir.go @@ -90,7 +90,7 @@ var rirRequiredFields = []string{ "last_updated", "name", "slug", - "tag", + "tags", } func resourceNetboxIpamRIRCreate(ctx context.Context, d *schema.ResourceData, From b2e2106b9e96cdf2049c3754434e7bbafa2127d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Fri, 18 Nov 2022 14:51:49 +0100 Subject: [PATCH 04/13] fix: Removing timezone from dcim_site fails fixes: 189 --- examples/resources/netbox_dcim_site/import.sh | 2 ++ netbox/dcim/resource_netbox_dcim_site.go | 6 +++++- netbox/dcim/resource_netbox_dcim_site_test.go | 3 +-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 examples/resources/netbox_dcim_site/import.sh diff --git a/examples/resources/netbox_dcim_site/import.sh b/examples/resources/netbox_dcim_site/import.sh new file mode 100644 index 000000000..d24800b8d --- /dev/null +++ b/examples/resources/netbox_dcim_site/import.sh @@ -0,0 +1,2 @@ +# Sites can be imported by id +terraform import netbox_dcim_site.site_test 1 diff --git a/netbox/dcim/resource_netbox_dcim_site.go b/netbox/dcim/resource_netbox_dcim_site.go index e2ca956ed..8a7d5782b 100644 --- a/netbox/dcim/resource_netbox_dcim_site.go +++ b/netbox/dcim/resource_netbox_dcim_site.go @@ -434,7 +434,11 @@ func resourceNetboxDcimSiteUpdate(ctx context.Context, d *schema.ResourceData, if d.HasChange("time_zone") { timeZone := d.Get("time_zone").(string) params.TimeZone = &timeZone - modifiedFields["time_zone"] = timeZone + if timeZone != "" { + modifiedFields["time_zone"] = timeZone + } else { + modifiedFields["time_zone"] = nil + } } resource := dcim.NewDcimSitesPartialUpdateParams().WithData(params) diff --git a/netbox/dcim/resource_netbox_dcim_site_test.go b/netbox/dcim/resource_netbox_dcim_site_test.go index f7b781efb..e4e3bb5e8 100644 --- a/netbox/dcim/resource_netbox_dcim_site_test.go +++ b/netbox/dcim/resource_netbox_dcim_site_test.go @@ -138,8 +138,7 @@ func testAccCheckNetboxDcimSiteConfig(nameSuffix string, resourceFull, extraReso latitude = 12.54632 longitude = 41.21632 tenant_id = netbox_tenancy_tenant.test.id - # Broken in netbox < 3.3.7 - # time_zone = "Europe/Berlin" + time_zone = "Europe/Berlin" comments = <<-EOT Comments for Test device role From a93a3c92ba51f69aed8a544551491ab62db9ea54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 25 Sep 2022 18:50:34 +0200 Subject: [PATCH 05/13] feat: Add site_id to cluster data source and to vm resource fixes: 191 --- .../netbox_virtualization_cluster/import.sh | 2 ++ .../data_netbox_virtualization_cluster.go | 7 +++++++ .../resource_netbox_virtualization_vm.go | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 examples/resources/netbox_virtualization_cluster/import.sh diff --git a/examples/resources/netbox_virtualization_cluster/import.sh b/examples/resources/netbox_virtualization_cluster/import.sh new file mode 100644 index 000000000..05e585494 --- /dev/null +++ b/examples/resources/netbox_virtualization_cluster/import.sh @@ -0,0 +1,2 @@ +# Clusters can be imported by id +terraform import netbox_virtualization_cluster.cluster_test 1 diff --git a/netbox/virtualization/data_netbox_virtualization_cluster.go b/netbox/virtualization/data_netbox_virtualization_cluster.go index 4df87c6bd..71f3cec14 100644 --- a/netbox/virtualization/data_netbox_virtualization_cluster.go +++ b/netbox/virtualization/data_netbox_virtualization_cluster.go @@ -26,6 +26,10 @@ func DataNetboxVirtualizationCluster() *schema.Resource { Required: true, ValidateFunc: validation.StringLenBetween(1, 100), }, + "site_id": { + Type: schema.TypeInt, + Computed: true, + }, }, } } @@ -57,6 +61,9 @@ func dataNetboxVirtualizationClusterRead(ctx context.Context, d *schema.Resource if err = d.Set("content_type", util.ConvertURIContentType(r.URL)); err != nil { return diag.FromErr(err) } + if err = d.Set("site_id", util.GetNestedSiteID(r.Site)); err != nil { + return diag.FromErr(err) + } return nil } diff --git a/netbox/virtualization/resource_netbox_virtualization_vm.go b/netbox/virtualization/resource_netbox_virtualization_vm.go index 98bc052b7..680561b58 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm.go @@ -102,6 +102,11 @@ func ResourceNetboxVirtualizationVM() *schema.Resource { Default: nil, Description: "ID of the role for this VM (virtualization module).", }, + "site_id": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the site where this VM (virtualization module) is attached. If cluster_id is set and the cluster resides in a site, this must be set and the same as the cluster's site", + }, "status": { Type: schema.TypeString, Optional: true, @@ -188,6 +193,10 @@ func resourceNetboxVirtualizationVMCreate(ctx context.Context, d *schema.Resourc newResource.Role = &roleID } + if siteID := int64(d.Get("site_id").(int)); siteID != 0 { + newResource.Site = &siteID + } + if tenantID != 0 { newResource.Tenant = &tenantID } @@ -284,6 +293,9 @@ func resourceNetboxVirtualizationVMRead(ctx context.Context, d *schema.ResourceD if err = d.Set("role_id", util.GetNestedRoleID(resource.Role)); 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("status", resource.Status.Value); err != nil { return diag.FromErr(err) @@ -391,6 +403,14 @@ func resourceNetboxVirtualizationVMUpdate(ctx context.Context, d *schema.Resourc } } + if d.HasChange("site_id") { + siteID := int64(d.Get("site_id").(int)) + params.Site = &siteID + if siteID == 0 { + emptyFields["site"] = nil + } + } + if d.HasChange("status") { status := d.Get("status").(string) params.Status = status From 74f5147d1e921192feab30dd9de2301e0167c558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Fri, 18 Nov 2022 15:47:20 +0100 Subject: [PATCH 06/13] fix: Status field missing from cluster resource fixes: #190 --- netbox/internal/util/nested.go | 8 ++++++++ .../resource_netbox_virtualization_cluster.go | 15 +++++++++++++++ ...resource_netbox_virtualization_cluster_test.go | 1 + 3 files changed, 24 insertions(+) diff --git a/netbox/internal/util/nested.go b/netbox/internal/util/nested.go index 7a7669859..da5ee62f7 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -4,6 +4,14 @@ import ( "github.com/smutel/go-netbox/v3/netbox/models" ) +func GetClusterStatusValue(nested *models.ClusterStatus) *string { + if nested == nil { + return nil + } + + return nested.Value +} + func GetNestedIPAddressAddress(nested *models.NestedIPAddress) *string { if nested == nil { return nil diff --git a/netbox/virtualization/resource_netbox_virtualization_cluster.go b/netbox/virtualization/resource_netbox_virtualization_cluster.go index 54f92b160..b8a830b10 100644 --- a/netbox/virtualization/resource_netbox_virtualization_cluster.go +++ b/netbox/virtualization/resource_netbox_virtualization_cluster.go @@ -72,6 +72,14 @@ func ResourceNetboxVirtualizationCluster() *schema.Resource { Optional: true, Description: "The site of this cluster.", }, + "status": { + Type: schema.TypeString, + Optional: true, + Default: "active", + ValidateFunc: validation.StringInSlice([]string{"offline", "active", + "planned", "staging", "decommissioning"}, false), + Description: "The status among offline, active, planned, staging or decommissioning (active by default).", + }, "tag": &tag.TagSchema, "tenant_id": { Type: schema.TypeInt, @@ -126,6 +134,7 @@ func resourceNetboxVirtualizationClusterCreate(ctx context.Context, d *schema.Re Name: &name, Tags: tag.ConvertTagsToNestedTags(tags), Type: &typeID, + Status: d.Get("status").(string), } if groupID != 0 { @@ -199,6 +208,9 @@ func resourceNetboxVirtualizationClusterRead(ctx context.Context, d *schema.Reso if err = d.Set("site_id", util.GetNestedSiteID(resource.Site)); err != nil { return diag.FromErr(err) } + if err = d.Set("status", util.GetClusterStatusValue(resource.Status)); err != nil { + return diag.FromErr(err) + } if err = d.Set("tag", tag.ConvertNestedTagsToTags(resource.Tags)); err != nil { return diag.FromErr(err) } @@ -247,6 +259,9 @@ func resourceNetboxVirtualizationClusterUpdate(ctx context.Context, d *schema.Re name := d.Get("name").(string) params.Name = &name } + if d.HasChange("status") { + params.Status = d.Get("status").(string) + } if d.HasChange("site_id") { siteID := int64(d.Get("site_id").(int)) params.Site = &siteID diff --git a/netbox/virtualization/resource_netbox_virtualization_cluster_test.go b/netbox/virtualization/resource_netbox_virtualization_cluster_test.go index 9171ca77f..ae6371238 100644 --- a/netbox/virtualization/resource_netbox_virtualization_cluster_test.go +++ b/netbox/virtualization/resource_netbox_virtualization_cluster_test.go @@ -126,6 +126,7 @@ func testAccCheckNetboxVirtualizationClusterConfig(nameSuffix string, resourceFu group_id = netbox_virtualization_cluster_group.test.id site_id = netbox_dcim_site.test.id tenant_id = netbox_tenancy_tenant.test.id + status = "decommissioning" comments = <<-EOT Test cluster From 214483026966252a292112399731b3b82e673480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Fri, 18 Nov 2022 15:58:00 +0100 Subject: [PATCH 07/13] feat: Update VM resource for Netbox 3.3 fixes: #192 - cluster_id is optional - site_id added - One of cluster_id, site_id must be provided - device_id added --- .../netbox_virtualization_vm/import.sh | 2 + netbox/internal/util/nested.go | 8 ++++ .../resource_netbox_virtualization_vm.go | 44 +++++++++++-------- 3 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 examples/resources/netbox_virtualization_vm/import.sh diff --git a/examples/resources/netbox_virtualization_vm/import.sh b/examples/resources/netbox_virtualization_vm/import.sh new file mode 100644 index 000000000..7d6701c69 --- /dev/null +++ b/examples/resources/netbox_virtualization_vm/import.sh @@ -0,0 +1,2 @@ +# Virtual machines can be imported by id +terraform import netbox_virtualization_vm.vm_test 1 diff --git a/netbox/internal/util/nested.go b/netbox/internal/util/nested.go index da5ee62f7..202a18138 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -28,6 +28,14 @@ func GetNestedClusterGroupID(nested *models.NestedClusterGroup) *int64 { return &nested.ID } +func GetNestedDeviceID(nested *models.NestedDevice) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} + func GetNestedManufacturerID(nested *models.NestedManufacturer) *int64 { if nested == nil { return nil diff --git a/netbox/virtualization/resource_netbox_virtualization_vm.go b/netbox/virtualization/resource_netbox_virtualization_vm.go index 680561b58..4b7156d46 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm.go @@ -33,7 +33,7 @@ func ResourceNetboxVirtualizationVM() *schema.Resource { Schema: map[string]*schema.Schema{ "cluster_id": { Type: schema.TypeInt, - Required: true, + Optional: true, Default: nil, Description: "ID of the cluster which host this VM (virtualization module).", }, @@ -49,6 +49,12 @@ func ResourceNetboxVirtualizationVM() *schema.Resource { Description: "The content type of this VM (virtualization module).", }, "custom_field": &customfield.CustomFieldSchema, + "device_id": { + Type: schema.TypeInt, + Optional: true, + Default: nil, + Description: "Optionally pin this VM to a specific host device within the cluster.", + }, "disk": { Type: schema.TypeInt, Optional: true, @@ -103,9 +109,10 @@ func ResourceNetboxVirtualizationVM() *schema.Resource { Description: "ID of the role for this VM (virtualization module).", }, "site_id": { - Type: schema.TypeInt, - Optional: true, - Description: "ID of the site where this VM (virtualization module) is attached. If cluster_id is set and the cluster resides in a site, this must be set and the same as the cluster's site", + Type: schema.TypeInt, + Optional: true, + Description: "ID of the site where this VM (virtualization module) is attached. If cluster_id is set and the cluster resides in a site, this must be set and the same as the cluster's site", + AtLeastOneOf: []string{"cluster_id", "site_id"}, }, "status": { Type: schema.TypeString, @@ -149,16 +156,9 @@ func resourceNetboxVirtualizationVMCreate(ctx context.Context, d *schema.Resourc comments := d.Get("comments").(string) resourceCustomFields := d.Get("custom_field").(*schema.Set).List() customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(nil, resourceCustomFields) - disk := int64(d.Get("disk").(int)) - localContextData := d.Get("local_context_data").(string) - memory := int64(d.Get("memory").(int)) name := d.Get("name").(string) - platformID := int64(d.Get("platform_id").(int)) - roleID := int64(d.Get("role_id").(int)) status := d.Get("status").(string) tags := d.Get("tag").(*schema.Set).List() - tenantID := int64(d.Get("tenant_id").(int)) - vcpus := d.Get("vcpus").(string) newResource := &models.WritableVirtualMachineWithConfigContext{ Cluster: &clusterID, @@ -169,15 +169,15 @@ func resourceNetboxVirtualizationVMCreate(ctx context.Context, d *schema.Resourc Tags: tag.ConvertTagsToNestedTags(tags), } - if disk != 0 { + if disk := int64(d.Get("disk").(int)); disk != 0 { newResource.Disk = &disk } - if memory != 0 { + if memory := int64(d.Get("memory").(int)); memory != 0 { newResource.Memory = &memory } - if localContextData != "" { + if localContextData := d.Get("local_context_data").(string); localContextData != "" { var localContextDataMap map[string]*interface{} if err := json.Unmarshal([]byte(localContextData), &localContextDataMap); err != nil { return diag.FromErr(err) @@ -185,11 +185,15 @@ func resourceNetboxVirtualizationVMCreate(ctx context.Context, d *schema.Resourc newResource.LocalContextData = localContextDataMap } - if platformID != 0 { + if deviceID := int64(d.Get("device_id").(int)); deviceID != 0 { + newResource.Device = &deviceID + } + + if platformID := int64(d.Get("platform_id").(int)); platformID != 0 { newResource.Platform = &platformID } - if roleID != 0 { + if roleID := int64(d.Get("role_id").(int)); roleID != 0 { newResource.Role = &roleID } @@ -197,11 +201,11 @@ func resourceNetboxVirtualizationVMCreate(ctx context.Context, d *schema.Resourc newResource.Site = &siteID } - if tenantID != 0 { + if tenantID := int64(d.Get("tenant_id").(int)); tenantID != 0 { newResource.Tenant = &tenantID } - if vcpus != "" { + if vcpus := d.Get("vcpus").(string); vcpus != "" { vcpusFloat, _ := strconv.ParseFloat(vcpus, 32) newResource.Vcpus = &vcpusFloat } @@ -257,6 +261,10 @@ func resourceNetboxVirtualizationVMRead(ctx context.Context, d *schema.ResourceD return diag.FromErr(err) } + if err = d.Set("device_id", util.GetNestedDeviceID(resource.Device)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("disk", resource.Disk); err != nil { return diag.FromErr(err) } From 53382b932bf4c3df9aadd7f2c2af8f11fdad3205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Mon, 14 Nov 2022 18:15:22 +0100 Subject: [PATCH 08/13] fix: Custom fields default values not null Fixes: 180 - Fix field default. - Add group_name and ui_visibility to customfield --- .../netbox_extras_custom_field/import.sh | 2 + .../resource_netbox_extras_custom_field.go | 67 ++++++++++++++++--- ...netbox_extras_custom_field_boolean_test.go | 17 ++--- ...ce_netbox_extras_custom_field_date_test.go | 17 ++--- ...netbox_extras_custom_field_integer_test.go | 5 +- ...ce_netbox_extras_custom_field_json_test.go | 29 ++++---- ...etbox_extras_custom_field_longtext_test.go | 4 +- ...ox_extras_custom_field_multiobject_test.go | 23 ++++--- ...ox_extras_custom_field_multiselect_test.go | 17 ++--- ..._netbox_extras_custom_field_object_test.go | 23 ++++--- ..._netbox_extras_custom_field_select_test.go | 16 +++-- ...ce_netbox_extras_custom_field_text_test.go | 4 +- ...rce_netbox_extras_custom_field_url_test.go | 17 ++--- netbox/internal/util/nested.go | 8 +++ 14 files changed, 162 insertions(+), 87 deletions(-) create mode 100644 examples/resources/netbox_extras_custom_field/import.sh diff --git a/examples/resources/netbox_extras_custom_field/import.sh b/examples/resources/netbox_extras_custom_field/import.sh new file mode 100644 index 000000000..6f461e366 --- /dev/null +++ b/examples/resources/netbox_extras_custom_field/import.sh @@ -0,0 +1,2 @@ +# Custom fields can be imported by id +terraform import netbox_extras_custom_field.cf_text 1 diff --git a/netbox/extras/resource_netbox_extras_custom_field.go b/netbox/extras/resource_netbox_extras_custom_field.go index 4c7af4b6e..e20acf8d1 100644 --- a/netbox/extras/resource_netbox_extras_custom_field.go +++ b/netbox/extras/resource_netbox_extras_custom_field.go @@ -2,6 +2,7 @@ package extras import ( "context" + "encoding/json" "strconv" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -63,7 +64,7 @@ func ResourceNetboxExtrasCustomField() *schema.Resource { "default": { Type: schema.TypeString, Optional: true, - Description: "The default value for this custom field. Encoded as JSON.", + Description: "The default value for this custom field. This value must be valid Json. Strings, List and Dicts should be wrapped in jsonencode()", }, "description": { Type: schema.TypeString, @@ -79,6 +80,13 @@ func ResourceNetboxExtrasCustomField() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{"disabled", "loose", "exact"}, false), Description: "The filter logic for this custom field. Allowed values: \"loose\" (default), \"exact\", \"disabled\"", }, + "group_name": { + Type: schema.TypeString, + Optional: true, + Default: nil, + ValidateFunc: validation.StringLenBetween(1, 50), + Description: "Custom fields within the same group will be displayed together.", + }, "label": { Type: schema.TypeString, Optional: true, @@ -123,6 +131,13 @@ func ResourceNetboxExtrasCustomField() *schema.Resource { }, false), Description: "Type of the custom field (text, longtext, integer, boolean, url, json, select, multiselect, object, multiobject).", }, + "ui_visibility": { + Type: schema.TypeString, + Optional: true, + Default: "read-write", + ValidateFunc: validation.StringInSlice([]string{"read-write", "read-only", "hidden"}, false), + Description: "The filter logic for this custom field. Allowed values: \"read-write\" (default), \"read-only\", \"hidden\"", + }, "url": { Type: schema.TypeString, Computed: true, @@ -167,7 +182,6 @@ func resourceNetboxExtrasCustomFieldCreate(ctx context.Context, d *schema.Resour client := m.(*netboxclient.NetBoxAPI) name := d.Get("name").(string) - defaultstring := d.Get("default").(string) validationMaximum := int64(d.Get("validation_maximum").(int)) var validationMaximumPtr *int64 if validationMaximum != 0 { @@ -187,20 +201,30 @@ func resourceNetboxExtrasCustomFieldCreate(ctx context.Context, d *schema.Resour newResource := &models.WritableCustomField{ Choices: util.ToListofStrings(d.Get("choices").(*schema.Set).List()), ContentTypes: util.ToListofStrings(d.Get("content_types").(*schema.Set).List()), - Default: &defaultstring, Description: d.Get("description").(string), FilterLogic: d.Get("filter_logic").(string), + GroupName: d.Get("group_name").(string), Label: d.Get("label").(string), Name: &name, ObjectType: d.Get("object_type").(string), Required: d.Get("required").(bool), Type: d.Get("type").(string), + UIVisibility: d.Get("ui_visibility").(string), ValidationMaximum: validationMaximumPtr, ValidationMinimum: validationMinimumPtr, ValidationRegex: d.Get("validation_regex").(string), Weight: &weight, } + if defaultstring := d.Get("default").(string); defaultstring != "" { + var jsonMap interface{} + err := json.Unmarshal([]byte(defaultstring), &jsonMap) + if err != nil { + return diag.FromErr(err) + } + newResource.Default = jsonMap + } + resource := extras.NewExtrasCustomFieldsCreateParams().WithData(newResource) resourceCreated, err := client.Extras.ExtrasCustomFieldsCreate(resource, nil) @@ -246,8 +270,15 @@ func resourceNetboxExtrasCustomFieldRead(ctx context.Context, d *schema.Resource if err = d.Set("data_type", resource.DataType); err != nil { return diag.FromErr(err) } - if err = d.Set("default", resource.Default); err != nil { - return diag.FromErr(err) + if resource.Default != nil { + jsonValue, _ := json.Marshal(resource.Default) + if err = d.Set("default", string(jsonValue)); err != nil { + return diag.FromErr(err) + } + } else { + if err = d.Set("default", nil); err != nil { + return diag.FromErr(err) + } } if err = d.Set("description", resource.Description); err != nil { return diag.FromErr(err) @@ -255,6 +286,9 @@ func resourceNetboxExtrasCustomFieldRead(ctx context.Context, d *schema.Resource if err = d.Set("filter_logic", resource.FilterLogic.Value); err != nil { return diag.FromErr(err) } + if err = d.Set("group_name", resource.GroupName); err != nil { + return diag.FromErr(err) + } if err = d.Set("label", resource.Label); err != nil { return diag.FromErr(err) } @@ -273,6 +307,9 @@ func resourceNetboxExtrasCustomFieldRead(ctx context.Context, d *schema.Resource if err = d.Set("type", resource.Type.Value); err != nil { return diag.FromErr(err) } + if err = d.Set("ui_visibility", util.GetCustomFieldUIVisibilityValue(resource.UIVisibility)); err != nil { + return diag.FromErr(err) + } if err = d.Set("url", resource.URL); err != nil { return diag.FromErr(err) } @@ -310,9 +347,16 @@ func resourceNetboxExtrasCustomFieldUpdate(ctx context.Context, d *schema.Resour params.ContentTypes = util.ToListofStrings(d.Get("content_types").(*schema.Set).List()) } if d.HasChange("default") { - defaultvalue := d.Get("default").(string) - params.Default = &defaultvalue - modifiedFields["default"] = defaultvalue + if defaultstring := d.Get("default").(string); defaultstring != "" { + var jsonMap interface{} + err := json.Unmarshal([]byte(defaultstring), &jsonMap) + if err != nil { + return diag.FromErr(err) + } + params.Default = jsonMap + } else { + modifiedFields["default"] = nil + } } if d.HasChange("description") { params.Description = d.Get("description").(string) @@ -321,6 +365,10 @@ func resourceNetboxExtrasCustomFieldUpdate(ctx context.Context, d *schema.Resour if d.HasChange("filter_logic") { params.FilterLogic = d.Get("filter_logic").(string) } + if d.HasChange("group_name") { + params.GroupName = d.Get("group_name").(string) + modifiedFields["group_name"] = params.GroupName + } if d.HasChange("label") { params.Label = d.Get("label").(string) modifiedFields["label"] = params.Label @@ -339,6 +387,9 @@ func resourceNetboxExtrasCustomFieldUpdate(ctx context.Context, d *schema.Resour if d.HasChange("type") { params.Type = d.Get("type").(string) } + if d.HasChange("ui_visibility") { + params.UIVisibility = d.Get("ui_visibility").(string) + } if d.HasChange("validation_maximum") { validationMaximum := int64(d.Get("validation_maximum").(int)) params.ValidationMaximum = &validationMaximum diff --git a/netbox/extras/resource_netbox_extras_custom_field_boolean_test.go b/netbox/extras/resource_netbox_extras_custom_field_boolean_test.go index 3cc80c64f..467bf8e34 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_boolean_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_boolean_test.go @@ -98,15 +98,16 @@ func testAccCheckNetboxExtrasCustomFieldBooleanConfig(nameSuffix string, resourc "dcim.site", ] - type = "boolean" + type = "boolean" {{ if eq .resourcefull "true" }} - description = "Test custom field" - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in Netbox 3.3 - #default = true + description = "Test custom field" + group_name = "testgroup" + ui_visibility = "hidden" + label = "Test Label for CF" + weight = 50 + #required = true + filter_logic = "disabled" + default = true {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_date_test.go b/netbox/extras/resource_netbox_extras_custom_field_date_test.go index 11751ad0e..1c9bbcc1e 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_date_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_date_test.go @@ -98,15 +98,16 @@ func testAccCheckNetboxExtrasCustomFieldDateConfig(nameSuffix string, resourceFu "dcim.site", ] - type = "date" + type = "date" {{ if eq .resourcefull "true" }} - description = "Test custom field" - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in Netbox 3.3 - #default = "2022-01-01" + description = "Test custom field" + group_name = "testgroup" + ui_visibility = "hidden" + label = "Test Label for CF" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode("2022-01-01") {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_integer_test.go b/netbox/extras/resource_netbox_extras_custom_field_integer_test.go index e3a24d171..85e7a5d6f 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_integer_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_integer_test.go @@ -101,12 +101,13 @@ func testAccCheckNetboxExtrasCustomFieldIntegerConfig(nameSuffix string, resourc type = "integer" {{ if eq .resourcefull "true" }} description = "Test custom field" + group_name = "testgroup" + ui_visibility = "hidden" label = "Test Label for CF" weight = 50 #required = true filter_logic = "disabled" - # Fixed in Netbox 3.3 - #default = 50 + default = 50 validation_minimum = 1 validation_maximum = 500 {{ end }} diff --git a/netbox/extras/resource_netbox_extras_custom_field_json_test.go b/netbox/extras/resource_netbox_extras_custom_field_json_test.go index b25efef3d..9e40f8738 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_json_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_json_test.go @@ -98,21 +98,22 @@ func testAccCheckNetboxExtrasCustomFieldJSONConfig(nameSuffix string, resourceFu "dcim.site", ] - type = "json" + type = "json" {{ if eq .resourcefull "true" }} - description = "Test custom field" - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in Netbox 3.3 - #default = jsonencode({ - # bool = false - # number = 1.5 - # dict = { - # text = "Some text"} - # } - #}) + description = "Test custom field" + group_name = "testgroup" + ui_visibility = "hidden" + label = "Test Label for CF" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode({ + bool = false + number = 1.5 + dict = { + text = "Some text" + } + }) {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_longtext_test.go b/netbox/extras/resource_netbox_extras_custom_field_longtext_test.go index 8e51c1496..51a63a973 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_longtext_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_longtext_test.go @@ -101,11 +101,13 @@ func testAccCheckNetboxExtrasCustomFieldLongtextConfig(nameSuffix string, resour type = "longtext" {{ if eq .resourcefull "true" }} description = "Test custom field" + group_name = "testgroup" + ui_visibility = "hidden" label = "Test Label for CF" weight = 50 #required = true filter_logic = "disabled" - default = "Default text" + default = jsonencode("Default text") validation_regex = "^.*$" {{ end }} } diff --git a/netbox/extras/resource_netbox_extras_custom_field_multiobject_test.go b/netbox/extras/resource_netbox_extras_custom_field_multiobject_test.go index 2a9fc2c0f..231b9086c 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_multiobject_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_multiobject_test.go @@ -105,18 +105,19 @@ func testAccCheckNetboxExtrasCustomFieldMultiObjectConfig(nameSuffix string, res "dcim.site", ] - type = "multiobject" - object_type = "dcim.platform" + type = "multiobject" + object_type = "dcim.platform" {{ if eq .resourcefull "true" }} - description = "Test custom field" - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in netbox 3.3 - #default = jsonencode([ - # netbox_dcim_platform.test.id - #]) + description = "Test custom field" + label = "Test Label for CF" + group_name = "testgroup" + ui_visibility = "hidden" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode([ + netbox_dcim_platform.test.id + ]) {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_multiselect_test.go b/netbox/extras/resource_netbox_extras_custom_field_multiselect_test.go index e9f4c438f..8e30a7846 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_multiselect_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_multiselect_test.go @@ -98,19 +98,20 @@ func testAccCheckNetboxExtrasCustomFieldMultiSelectConfig(nameSuffix string, res "dcim.site", ] - type = "multiselect" + type = "multiselect" {{ if eq .resourcefull "true" }} - description = "Test custom field" + description = "Test custom field" choices = [ "test", "test2" ] - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in netbox 3.3 - #default = "test" + label = "Test Label for CF" + group_name = "testgroup" + ui_visibility = "hidden" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode(["test"]) {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_object_test.go b/netbox/extras/resource_netbox_extras_custom_field_object_test.go index d04e42594..cf4409df3 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_object_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_object_test.go @@ -105,18 +105,19 @@ func testAccCheckNetboxExtrasCustomFieldObjectConfig(nameSuffix string, resource "dcim.site", ] - type = "object" - object_type = "dcim.platform" + type = "object" + object_type = "dcim.platform" {{ if eq .resourcefull "true" }} - description = "Test custom field" - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in netbox 3.3 - #default = jsonencode([ - # netbox_dcim_platform.test.id - #]) + description = "Test custom field" + group_name = "testgroup" + ui_visibility = "hidden" + label = "Test Label for CF" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode([ + netbox_dcim_platform.test.id + ]) {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_select_test.go b/netbox/extras/resource_netbox_extras_custom_field_select_test.go index 4cf39f072..a01806f4d 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_select_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_select_test.go @@ -98,18 +98,20 @@ func testAccCheckNetboxExtrasCustomFieldSelectConfig(nameSuffix string, resource "dcim.site", ] - type = "select" + type = "select" {{ if eq .resourcefull "true" }} - description = "Test custom field" + description = "Test custom field" choices = [ "test", "test2" ] - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - default = "test" + label = "Test Label for CF" + group_name = "testgroup" + ui_visibility = "hidden" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode("test") {{ end }} } ` diff --git a/netbox/extras/resource_netbox_extras_custom_field_text_test.go b/netbox/extras/resource_netbox_extras_custom_field_text_test.go index 7dafedad7..b0daa1cf9 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_text_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_text_test.go @@ -102,10 +102,12 @@ func testAccCheckNetboxExtrasCustomFieldTextConfig(nameSuffix string, resourceFu {{ if eq .resourcefull "true" }} description = "Test custom field" label = "Test Label for CF" + group_name = "testgroup" + ui_visibility = "hidden" weight = 50 #required = true filter_logic = "disabled" - default = "Default text" + default = jsonencode("Default text") validation_regex = "^.*$" {{ end }} } diff --git a/netbox/extras/resource_netbox_extras_custom_field_url_test.go b/netbox/extras/resource_netbox_extras_custom_field_url_test.go index 3a1a3cded..5ef50d748 100644 --- a/netbox/extras/resource_netbox_extras_custom_field_url_test.go +++ b/netbox/extras/resource_netbox_extras_custom_field_url_test.go @@ -98,15 +98,16 @@ func testAccCheckNetboxExtrasCustomFieldURLConfig(nameSuffix string, resourceFul "dcim.site", ] - type = "url" + type = "url" {{ if eq .resourcefull "true" }} - description = "Test custom field" - label = "Test Label for CF" - weight = 50 - #required = true - filter_logic = "disabled" - # Fixed in Netbox 3.3 - #default = "https://netbox.dev/" + description = "Test custom field" + label = "Test Label for CF" + group_name = "testgroup" + ui_visibility = "hidden" + weight = 50 + #required = true + filter_logic = "disabled" + default = jsonencode("https://netbox.dev/") {{ end }} } ` diff --git a/netbox/internal/util/nested.go b/netbox/internal/util/nested.go index 202a18138..00086a896 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -12,6 +12,14 @@ func GetClusterStatusValue(nested *models.ClusterStatus) *string { return nested.Value } +func GetCustomFieldUIVisibilityValue(nested *models.CustomFieldUIVisibility) *string { + if nested == nil { + return nil + } + + return nested.Value +} + func GetNestedIPAddressAddress(nested *models.NestedIPAddress) *string { if nested == nil { return nil 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 09/13] 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) +} From 18ec0e8e8861ea4a639f2a9dfc47b95b7fd4a550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Mon, 31 Oct 2022 19:36:00 +0100 Subject: [PATCH 10/13] fix: Trim spaces for vm comment field Fixes: 194 --- .../resource_netbox_virtualization_vm.go | 1 + .../resource_netbox_virtualization_vm_test.go | 27 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/netbox/virtualization/resource_netbox_virtualization_vm.go b/netbox/virtualization/resource_netbox_virtualization_vm.go index 4b7156d46..c24ddd5fd 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm.go @@ -41,6 +41,7 @@ func ResourceNetboxVirtualizationVM() *schema.Resource { Type: schema.TypeString, Optional: true, Default: nil, + StateFunc: util.TrimString, Description: "Comments for this VM (virtualization module).", }, "content_type": { diff --git a/netbox/virtualization/resource_netbox_virtualization_vm_test.go b/netbox/virtualization/resource_netbox_virtualization_vm_test.go index 6a47ac43d..38910dc87 100644 --- a/netbox/virtualization/resource_netbox_virtualization_vm_test.go +++ b/netbox/virtualization/resource_netbox_virtualization_vm_test.go @@ -92,17 +92,14 @@ func TestAccNetboxVirtualizationVMMinimalFullMinimal(t *testing.T) { func testAccCheckNetboxVirtualizationVMConfig(nameSuffix string, resourceFull, extraResources 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 Cluster" + 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 } {{ if eq .extraresources "true" }} @@ -129,11 +126,13 @@ func testAccCheckNetboxVirtualizationVMConfig(nameSuffix string, resourceFull, e 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 .resourcefull "true" }} - comments = "VM created by terraform" + comments = <<-EOT + VM created by terraform + Multiline + EOT role_id = netbox_dcim_device_role.test.id platform_id = netbox_dcim_platform.test.id tenant_id = netbox_tenancy_tenant.test.id From 89f477cbb2eb187dd222e355175c4cade89f86a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Tue, 1 Nov 2022 11:05:26 +0100 Subject: [PATCH 11/13] feat: Add fields and remove forcenew from netbox_virtualization_interface Fixes: 195 --- .../netbox_virtualization_interface/import.sh | 2 + netbox/internal/util/nested.go | 8 + netbox/internal/util/testing.go | 23 ++ netbox/internal/util/util.go | 22 +- ...esource_netbox_virtualization_interface.go | 263 ++++++++++++++---- ...ce_netbox_virtualization_interface_test.go | 179 ++++++++++++ 6 files changed, 441 insertions(+), 56 deletions(-) create mode 100644 examples/resources/netbox_virtualization_interface/import.sh create mode 100644 netbox/virtualization/resource_netbox_virtualization_interface_test.go diff --git a/examples/resources/netbox_virtualization_interface/import.sh b/examples/resources/netbox_virtualization_interface/import.sh new file mode 100644 index 000000000..11eac50b8 --- /dev/null +++ b/examples/resources/netbox_virtualization_interface/import.sh @@ -0,0 +1,2 @@ +# Virtualization interfaces can be imported by id +terraform import netbox_virtualization_interface.interface_test 1 diff --git a/netbox/internal/util/nested.go b/netbox/internal/util/nested.go index 00086a896..992af88fd 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -99,3 +99,11 @@ func GetNestedRoleID(nested *models.NestedDeviceRole) *int64 { return &nested.ID } + +func GetNestedVlanID(nested *models.NestedVLAN) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} diff --git a/netbox/internal/util/testing.go b/netbox/internal/util/testing.go index ff7ac6251..0d424e815 100644 --- a/netbox/internal/util/testing.go +++ b/netbox/internal/util/testing.go @@ -47,3 +47,26 @@ func TestAccPreCheck(t *testing.T) { t.Fatal("NETBOX_TOKEN must be set for acceptance tests") } } + +func TestAccSaveID(n string, id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID set") + } + + *id = rs.Primary.ID + return nil + } +} + +func TestAccCheckID(r, k string, id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + return resource.TestCheckResourceAttr(r, k, *id)(s) + } +} diff --git a/netbox/internal/util/util.go b/netbox/internal/util/util.go index 1fc8b3319..74f13207f 100644 --- a/netbox/internal/util/util.go +++ b/netbox/internal/util/util.go @@ -23,6 +23,18 @@ func ConvertNestedASNsToASNs(asns []*models.NestedASN) []int64 { return tfASNs } +func ConvertNestedVlansToVlans(vlans []*models.NestedVLAN) []int64 { + var tfVlans []int64 + + for _, t := range vlans { + vlan := t.ID + + tfVlans = append(tfVlans, vlan) + } + + return tfVlans +} + // Convert URL in content_type func ConvertURIContentType(uri strfmt.URI) string { uriSplit := strings.Split(uri.String(), "/") @@ -37,15 +49,17 @@ func ConvertURIContentType(uri strfmt.URI) string { return contentType } -func ExpandToInt64Slice(v []interface{}) []int64 { +func ExpandToInt64Slice(v []interface{}) ([]int64, error) { s := make([]int64, len(v)) for i, val := range v { - if strVal, ok := val.(int64); ok { - s[i] = strVal + if strVal, ok := val.(int); ok { + s[i] = int64(strVal) + } else { + return nil, fmt.Errorf("could not convert values to int") } } - return s + return s, nil } func Float2stringptr(vcpus *float64) *string { diff --git a/netbox/virtualization/resource_netbox_virtualization_interface.go b/netbox/virtualization/resource_netbox_virtualization_interface.go index 953190bb7..728f375a5 100644 --- a/netbox/virtualization/resource_netbox_virtualization_interface.go +++ b/netbox/virtualization/resource_netbox_virtualization_interface.go @@ -12,6 +12,7 @@ import ( "github.com/smutel/go-netbox/v3/netbox/client/virtualization" "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" ) @@ -32,11 +33,31 @@ func ResourceNetboxVirtualizationInterface() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "bridge_id": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the bridge interface where this interface (virtualization module) is attached to.", + }, "content_type": { Type: schema.TypeString, Computed: true, Description: "The content type of this interface (virtualization module).", }, + "count_fhrp_groups": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of fhrp groups attached to this interface (virtualization module) is attached to.", + }, + "count_ipaddresses": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of ip addresses attached to this interface (virtualization module) is attached to.", + }, + "created": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this resource was created.", + }, "custom_field": &customfield.CustomFieldSchema, "description": { Type: schema.TypeString, @@ -51,13 +72,17 @@ func ResourceNetboxVirtualizationInterface() *schema.Resource { Default: true, Description: "true or false (true by default)", }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this resource was last updated.", + }, "mac_address": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringMatch( regexp.MustCompile("^([A-Z0-9]{2}:){5}[A-Z0-9]{2}$"), "Must be like AA:AA:AA:AA:AA"), - ForceNew: true, Description: "Mac address for this interface (virtualization module)", }, "mode": { @@ -65,14 +90,12 @@ func ResourceNetboxVirtualizationInterface() *schema.Resource { Optional: true, ValidateFunc: validation.StringInSlice([]string{"access", "tagged", "tagged-all"}, false), - ForceNew: true, Description: "The mode among access, tagged, tagged-all.", }, "mtu": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(1, 65536), - ForceNew: true, Description: "The MTU between 1 and 65536 for this interface (virtualization module).", }, "name": { @@ -81,6 +104,11 @@ func ResourceNetboxVirtualizationInterface() *schema.Resource { ValidateFunc: validation.StringLenBetween(1, 64), Description: "Description for this interface (virtualization module)", }, + "parent_id": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the parent interface where this interface (virtualization module) is attached to.", + }, "tagged_vlans": { Type: schema.TypeSet, Elem: &schema.Schema{ @@ -95,6 +123,11 @@ func ResourceNetboxVirtualizationInterface() *schema.Resource { Computed: true, Description: "Type of interface among virtualization.vminterface for VM or dcim.interface for device", }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The link to this interface (virtualization module).", + }, "untagged_vlan": { Type: schema.TypeInt, Optional: true, @@ -105,6 +138,11 @@ func ResourceNetboxVirtualizationInterface() *schema.Resource { Required: true, Description: "ID of the VM where this interface (virtualization module) is attached to.", }, + "vrf_id": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the VRF where this interface (virtualization module) is attached to.", + }, }, } } @@ -113,15 +151,23 @@ func resourceNetboxVirtualizationInterfaceCreate(ctx context.Context, d *schema. m interface{}) diag.Diagnostics { client := m.(*netboxclient.NetBoxAPI) + dropFields := []string{ + "created", + "last_updated", + } + emptyFields := make(map[string]interface{}) + resourceCustomFields := d.Get("custom_field").(*schema.Set).List() customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(nil, resourceCustomFields) description := d.Get("description").(string) enabled := d.Get("enabled").(bool) - macAddress := d.Get("mac_address").(string) mode := d.Get("mode").(string) mtu := int64(d.Get("mtu").(int)) name := d.Get("name").(string) - taggedVlans := d.Get("tagged_vlans").(*schema.Set).List() + taggedVlans, err := util.ExpandToInt64Slice(d.Get("tagged_vlans").(*schema.Set).List()) + if err != nil { + return diag.FromErr(err) + } tags := d.Get("tag").(*schema.Set).List() untaggedVlan := int64(d.Get("untagged_vlan").(int)) virtualmachineID := int64(d.Get("virtualmachine_id").(int)) @@ -132,15 +178,31 @@ func resourceNetboxVirtualizationInterfaceCreate(ctx context.Context, d *schema. Enabled: enabled, Mode: mode, Name: &name, - TaggedVlans: util.ExpandToInt64Slice(taggedVlans), + TaggedVlans: taggedVlans, Tags: tag.ConvertTagsToNestedTags(tags), VirtualMachine: &virtualmachineID, } - if macAddress != "" { + if !enabled { + emptyFields["enabled"] = false + } + + if macAddress := d.Get("mac_address").(string); macAddress != "" { newResource.MacAddress = &macAddress } + if bridgeID := int64(d.Get("bridge_id").(int)); bridgeID != 0 { + newResource.Bridge = &bridgeID + } + + if parentID := int64(d.Get("parent_id").(int)); parentID != 0 { + newResource.Parent = &parentID + } + + if vrfID := int64(d.Get("vrf_id").(int)); vrfID != 0 { + newResource.Vrf = &vrfID + } + if mtu != 0 { newResource.Mtu = &mtu } @@ -152,7 +214,7 @@ func resourceNetboxVirtualizationInterfaceCreate(ctx context.Context, d *schema. resource := virtualization.NewVirtualizationInterfacesCreateParams().WithData(newResource) resourceCreated, err := client.Virtualization.VirtualizationInterfacesCreate( - resource, nil) + resource, nil, requestmodifier.NewRequestModifierOperation(emptyFields, dropFields)) if err != nil { return diag.FromErr(err) @@ -182,6 +244,23 @@ func resourceNetboxVirtualizationInterfaceRead(ctx context.Context, d *schema.Re if err = d.Set("content_type", util.ConvertURIContentType(resource.URL)); err != nil { return diag.FromErr(err) } + var bridgeID *int64 + bridgeID = nil + if resource.Bridge != nil { + bridgeID = &resource.Bridge.ID + } + if err = d.Set("bridge_id", bridgeID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("count_fhrp_groups", resource.CountFhrpGroups); err != nil { + return diag.FromErr(err) + } + if err = d.Set("count_ipaddresses", resource.CountIpaddresses); 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) @@ -190,33 +269,28 @@ func resourceNetboxVirtualizationInterfaceRead(ctx context.Context, d *schema.Re return diag.FromErr(err) } - var description interface{} - if resource.Description == "" { - description = nil - } else { - description = resource.Description - } - - if err = d.Set("description", description); err != nil { + if err = d.Set("description", resource.Description); err != nil { return diag.FromErr(err) } if err = d.Set("enabled", resource.Enabled); 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("mac_address", resource.MacAddress); err != nil { return diag.FromErr(err) } - if resource.Mode == nil { - if err = d.Set("mode", ""); err != nil { - return diag.FromErr(err) - } - } else { - if err = d.Set("mode", resource.Mode.Value); err != nil { - return diag.FromErr(err) - } + var mode *string + mode = nil + if resource.Mode != nil { + mode = resource.Mode.Value + } + if err = d.Set("mode", mode); err != nil { + return diag.FromErr(err) } if err = d.Set("mtu", resource.Mtu); err != nil { @@ -226,8 +300,16 @@ func resourceNetboxVirtualizationInterfaceRead(ctx context.Context, d *schema.Re if err = d.Set("name", resource.Name); err != nil { return diag.FromErr(err) } + var parentID *int64 + parentID = nil + if resource.Parent != nil { + parentID = &resource.Parent.ID + } + if err = d.Set("parent_id", parentID); err != nil { + return diag.FromErr(err) + } - if err = d.Set("tagged_vlans", resource.TaggedVlans); err != nil { + if err = d.Set("tagged_vlans", util.ConvertNestedVlansToVlans(resource.TaggedVlans)); err != nil { return diag.FromErr(err) } @@ -236,19 +318,21 @@ func resourceNetboxVirtualizationInterfaceRead(ctx context.Context, d *schema.Re return diag.FromErr(err) } - if err = d.Set("untagged_vlan", resource.UntaggedVlan); err != nil { + if err = d.Set("untagged_vlan", util.GetNestedVlanID(resource.UntaggedVlan)); err != nil { return diag.FromErr(err) } - if resource.VirtualMachine == nil { - if err = d.Set("virtualmachine_id", 0); err != nil { - return diag.FromErr(err) - } - } else { - if err = d.Set("virtualmachine_id", - resource.VirtualMachine.ID); err != nil { - return diag.FromErr(err) - } + if err = d.Set("virtualmachine_id", + resource.VirtualMachine.ID); err != nil { + return diag.FromErr(err) + } + var vrfID *int64 + vrfID = nil + if resource.Vrf != nil { + vrfID = &resource.Vrf.ID + } + if err = d.Set("vrf_id", vrfID); err != nil { + return diag.FromErr(err) } if err = d.Set("type", vMInterfaceType); err != nil { @@ -267,14 +351,20 @@ func resourceNetboxVirtualizationInterfaceUpdate(ctx context.Context, d *schema. m interface{}) diag.Diagnostics { client := m.(*netboxclient.NetBoxAPI) params := &models.WritableVMInterface{} + dropFields := []string{ + "created", + "last_updated", + } + emptyFields := make(map[string]interface{}) - // Required parameters - name := d.Get("name").(string) - params.Name = &name - virtualMachineID := int64(d.Get("virtualmachine_id").(int)) - params.VirtualMachine = &virtualMachineID - taggedVlans := d.Get("tagged_vlans").(*schema.Set).List() - params.TaggedVlans = util.ExpandToInt64Slice(taggedVlans) + if d.HasChange("bridge_id") { + bridgeID := int64(d.Get("bridge_id").(int)) + if bridgeID != 0 { + params.Bridge = &bridgeID + } else { + emptyFields["bridge"] = nil + } + } if d.HasChange("custom_field") { stateCustomFields, resourceCustomFields := d.GetChange("custom_field") @@ -283,39 +373,108 @@ func resourceNetboxVirtualizationInterfaceUpdate(ctx context.Context, d *schema. } if d.HasChange("description") { - if description, exist := d.GetOk("description"); exist { - params.Description = description.(string) + description := d.Get("description").(string) + if description != "" { + params.Description = description } else { - params.Description = " " + emptyFields["description"] = "" } } if d.HasChange("enabled") { enabled := d.Get("enabled").(bool) - params.Enabled = enabled + if enabled { + params.Enabled = enabled + } else { + emptyFields["enabled"] = false + } } if d.HasChange("mac_address") { macAddress := d.Get("mac_address").(string) - params.MacAddress = &macAddress + if macAddress != "" { + params.MacAddress = &macAddress + } else { + emptyFields["mac_address"] = nil + } } if d.HasChange("mode") { mode := d.Get("mode").(string) - params.Mode = mode + if mode != "" { + params.Mode = mode + } else { + emptyFields["mode"] = nil + } } if d.HasChange("mtu") { mtu := int64(d.Get("mtu").(int)) - params.Mtu = &mtu + if mtu != 0 { + params.Mtu = &mtu + } else { + emptyFields["mtu"] = nil + } } - tags := d.Get("tag").(*schema.Set).List() - params.Tags = tag.ConvertTagsToNestedTags(tags) + if d.HasChange("name") { + name := d.Get("name").(string) + params.Name = &name + } else { + dropFields = append(dropFields, "name") + } + + if d.HasChange("parent_id") { + parentID := int64(d.Get("parent_id").(int)) + if parentID != 0 { + params.Parent = &parentID + } else { + emptyFields["parent"] = nil + } + } + + if d.HasChange("tag") { + tags := d.Get("tag").(*schema.Set).List() + params.Tags = tag.ConvertTagsToNestedTags(tags) + } else { + dropFields = append(dropFields, "tags") + } + + if d.HasChange("tagged_vlans") { + taggedVlans := d.Get("tagged_vlans").(*schema.Set).List() + tvlans, err := util.ExpandToInt64Slice(taggedVlans) + if err != nil { + return diag.FromErr(err) + } + params.TaggedVlans = tvlans + } else { + dropFields = append(dropFields, "tagged_vlans") + } if d.HasChange("untagged_vlan") { untaggedVlan := int64(d.Get("untagged_vlan").(int)) params.UntaggedVlan = &untaggedVlan + if untaggedVlan == 0 { + emptyFields["untagged_vlan"] = nil + } + } + + if d.HasChange("virtual_machine_id") { + virtualMachineID := int64(d.Get("virtual_machine_id").(int)) + if virtualMachineID != 0 { + params.VirtualMachine = &virtualMachineID + } + } else { + dropFields = append(dropFields, "virtual_machine") + } + + if d.HasChange("vrf_id") { + vrfID := int64(d.Get("vrf_id").(int)) + if vrfID != 0 { + params.Vrf = &vrfID + } else { + emptyFields["vrf"] = nil + } } resource := virtualization.NewVirtualizationInterfacesPartialUpdateParams().WithData(params) @@ -328,7 +487,7 @@ func resourceNetboxVirtualizationInterfaceUpdate(ctx context.Context, d *schema. resource.SetID(resourceID) _, err = client.Virtualization.VirtualizationInterfacesPartialUpdate( - resource, nil) + resource, nil, requestmodifier.NewRequestModifierOperation(emptyFields, dropFields)) if err != nil { return diag.FromErr(err) } diff --git a/netbox/virtualization/resource_netbox_virtualization_interface_test.go b/netbox/virtualization/resource_netbox_virtualization_interface_test.go new file mode 100644 index 000000000..77e02f613 --- /dev/null +++ b/netbox/virtualization/resource_netbox_virtualization_interface_test.go @@ -0,0 +1,179 @@ +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/v6/netbox/internal/util" +) + +const resourceNameNetboxVirtualizationInterface = "netbox_virtualization_interface.test" + +func TestAccNetboxVirtualizationInterfaceMinimal(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: testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationInterface), + ), + }, + { + ResourceName: resourceNameNetboxVirtualizationInterface, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxVirtualizationInterfaceFull(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: testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationInterface), + ), + }, + { + ResourceName: resourceNameNetboxVirtualizationInterface, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxVirtualizationInterfaceMinimalFullMinimal(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + // var resourceID *string + // tmp := "" + resourceID := "0" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationInterface), + util.TestAccSaveID(resourceNameNetboxVirtualizationInterface, &resourceID), + ), + }, + { + Config: testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationInterface), + util.TestAccCheckID(resourceNameNetboxVirtualizationInterface, "id", &resourceID), + ), + }, + { + Config: testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix, false, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationInterface), + util.TestAccCheckID(resourceNameNetboxVirtualizationInterface, "id", &resourceID), + ), + }, + { + Config: testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameNetboxVirtualizationInterface), + util.TestAccCheckID(resourceNameNetboxVirtualizationInterface, "id", &resourceID), + ), + }, + }, + }) +} + +func testAccCheckNetboxVirtualizationInterfaceConfig(nameSuffix string, resourceFull, extraResources 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 + } + + resource "netbox_virtualization_vm" "test" { + name = "test-{{ .namesuffix }}" + cluster_id = netbox_virtualization_cluster.test.id + } + + {{ if eq .extraresources "true" }} + resource "netbox_virtualization_interface" "bridge" { + name = "bridge-{{ .namesuffix }}" + virtualmachine_id = netbox_virtualization_vm.test.id + } + + resource "netbox_virtualization_interface" "parent" { + name = "parent-{{ .namesuffix }}" + virtualmachine_id = netbox_virtualization_vm.test.id + } + + resource "netbox_extras_tag" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_ipam_vlan" "tagged" { + name = "test-{{ .namesuffix }}" + vlan_id = 1501 + } + + resource "netbox_ipam_vlan" "untagged" { + name = "test-{{ .namesuffix }}" + vlan_id = 1101 + } + + #resource "netbox_ipam_vrf" "test" { + # name = "test-{{ .namesuffix }}" + #} + {{ end }} + + resource "netbox_virtualization_interface" "test" { + name = "test-{{ .namesuffix }}" + virtualmachine_id = netbox_virtualization_vm.test.id + + {{ if eq .resourcefull "true" }} + description = "Test interface" + mac_address = "AA:AA:AA:AA:AA:AA" + mtu = 1300 + enabled = false + mode = "tagged" + tagged_vlans = [ + netbox_ipam_vlan.tagged.id, + ] + untagged_vlan = netbox_ipam_vlan.untagged.id + parent_id = netbox_virtualization_interface.parent.id + bridge_id = netbox_virtualization_interface.bridge.id + #vrf_id = netbox_ipam_vrf.test.id + + 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) +} From df80a603255703040d392a2be19ffdaea158fa4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 12 Mar 2023 10:47:24 +0100 Subject: [PATCH 12/13] feat: Update ipam_ip_addresses Fixes: #196 --- .../netbox_ipam_ip_addresses/import.sh | 2 + netbox/internal/util/nested.go | 48 +++ netbox/internal/util/util.go | 12 + .../ipam/resource_netbox_ipam_ip_addresses.go | 370 ++++++++---------- .../resource_netbox_ipam_ip_addresses_test.go | 168 ++++++++ netbox/ipam/util.go | 25 ++ 6 files changed, 427 insertions(+), 198 deletions(-) create mode 100644 examples/resources/netbox_ipam_ip_addresses/import.sh create mode 100644 netbox/ipam/resource_netbox_ipam_ip_addresses_test.go diff --git a/examples/resources/netbox_ipam_ip_addresses/import.sh b/examples/resources/netbox_ipam_ip_addresses/import.sh new file mode 100644 index 000000000..aac8e02c9 --- /dev/null +++ b/examples/resources/netbox_ipam_ip_addresses/import.sh @@ -0,0 +1,2 @@ +# IP addresses can be imported by id +terraform import netbox_ipam_ip_addresses.ip_test 1 diff --git a/netbox/internal/util/nested.go b/netbox/internal/util/nested.go index 992af88fd..0a241914b 100644 --- a/netbox/internal/util/nested.go +++ b/netbox/internal/util/nested.go @@ -20,6 +20,38 @@ func GetCustomFieldUIVisibilityValue(nested *models.CustomFieldUIVisibility) *st return nested.Value } +func GetIPAddressAssignedObject(nested *models.IPAddress) (*string, *int64) { + if nested == nil { + return nil, nil + } + + return nested.AssignedObjectType, nested.AssignedObjectID +} + +func GetIPAddressFamilyLabel(nested *models.IPAddressFamily) *string { + if nested == nil { + return nil + } + + return nested.Label +} + +func GetIPAddressRoleValue(nested *models.IPAddressRole) *string { + if nested == nil { + return nil + } + + return nested.Value +} + +func GetIPAddressStatusValue(nested *models.IPAddressStatus) *string { + if nested == nil { + return nil + } + + return nested.Value +} + func GetNestedIPAddressAddress(nested *models.NestedIPAddress) *string { if nested == nil { return nil @@ -28,6 +60,14 @@ func GetNestedIPAddressAddress(nested *models.NestedIPAddress) *string { return nested.Address } +func GetNestedIPAddressID(nested *models.NestedIPAddress) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} + func GetNestedClusterGroupID(nested *models.NestedClusterGroup) *int64 { if nested == nil { return nil @@ -107,3 +147,11 @@ func GetNestedVlanID(nested *models.NestedVLAN) *int64 { return &nested.ID } + +func GetNestedVrfID(nested *models.NestedVRF) *int64 { + if nested == nil { + return nil + } + + return &nested.ID +} diff --git a/netbox/internal/util/util.go b/netbox/internal/util/util.go index 74f13207f..beb79e5a5 100644 --- a/netbox/internal/util/util.go +++ b/netbox/internal/util/util.go @@ -23,6 +23,18 @@ func ConvertNestedASNsToASNs(asns []*models.NestedASN) []int64 { return tfASNs } +func ConvertNestedIPsToIPs(asns []*models.NestedIPAddress) []int64 { + var tfASNs []int64 + + for _, t := range asns { + asn := t.ID + + tfASNs = append(tfASNs, asn) + } + + return tfASNs +} + func ConvertNestedVlansToVlans(vlans []*models.NestedVLAN) []int64 { var tfVlans []int64 diff --git a/netbox/ipam/resource_netbox_ipam_ip_addresses.go b/netbox/ipam/resource_netbox_ipam_ip_addresses.go index 25139a4c0..c4d3305c1 100644 --- a/netbox/ipam/resource_netbox_ipam_ip_addresses.go +++ b/netbox/ipam/resource_netbox_ipam_ip_addresses.go @@ -12,6 +12,7 @@ import ( "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" ) @@ -37,17 +38,10 @@ func ResourceNetboxIpamIPAddresses() *schema.Resource { ValidateFunc: validation.IsCIDR, Description: "The IP address (with mask) used for this IP address (ipam module). Required if both prefix and ip_range are not set.", }, - "ip_range": { - Type: schema.TypeInt, - ForceNew: true, - Optional: true, - Description: "The ip-range id for automatic IP assignment. Required if both prefix and address are not set.", - }, - "prefix": { - Type: schema.TypeInt, - ForceNew: true, - Optional: true, - Description: "The prefix id for automatic IP assignment. Required if both address and ip_range are not set.", + "created": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this aggregate was created.", }, "content_type": { Type: schema.TypeString, @@ -59,7 +53,7 @@ func ResourceNetboxIpamIPAddresses() *schema.Resource { Type: schema.TypeString, Optional: true, Default: nil, - ValidateFunc: validation.StringLenBetween(1, 100), + ValidateFunc: validation.StringLenBetween(1, 200), Description: "The description of this IP address (ipam module).", }, "dns_name": { @@ -71,24 +65,52 @@ func ResourceNetboxIpamIPAddresses() *schema.Resource { "Must be like ^[-a-zA-Z0-9_.]{1,255}$"), Description: "The DNS name of this IP address (ipam module).", }, + "family": { + Type: schema.TypeString, + Computed: true, + Description: "Family of IP address (IPv4 or IPv6).", + }, + "ip_range": { + Type: schema.TypeInt, + ForceNew: true, + Optional: true, + Description: "The ip-range id for automatic IP assignment. Required if both prefix and address are not set.", + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this aggregate was last updated.", + }, "nat_inside_id": { Type: schema.TypeInt, Optional: true, Description: "The ID of the NAT inside of this IP address (ipam module).", }, + // "nat_outside": { + // Type: schema.TypeList, + // Computed: true, + // Description: "The IDs of the NAT outside of this IP address (ipam module).", + // Elem: schema.TypeInt, + // }, "object_id": { - Type: schema.TypeInt, - Optional: true, - ForceNew: true, - Description: "The ID of the object where this resource is attached to.", + Type: schema.TypeInt, + Optional: true, + Description: "The ID of the object where this resource is attached to.", + RequiredWith: []string{"object_type"}, }, "object_type": { Type: schema.TypeString, Optional: true, - Default: "", + Default: nil, ValidateFunc: validation.StringInSlice([]string{ - vMInterfaceType, "dcim.interface"}, false), - Description: "The object type among virtualization.vminterface or dcim.interface (empty by default).", + vMInterfaceType, deviceInterfaceType, fhrpgroupType}, false), + Description: "The object type among virtualization.vminterface, dcim.interface or ipam.fhrpgroup (empty by default).", + }, + "prefix": { + Type: schema.TypeInt, + ForceNew: true, + Optional: true, + Description: "The prefix id for automatic IP assignment. Required if both address and ip_range are not set.", }, "primary_ip4": { Type: schema.TypeBool, @@ -113,9 +135,9 @@ func ResourceNetboxIpamIPAddresses() *schema.Resource { Type: schema.TypeString, Optional: true, Default: "active", - ValidateFunc: validation.StringInSlice([]string{"container", "active", - "reserved", "deprecated", "dhcp"}, false), - Description: "The status among of this IP address (ipam module) container, active, reserved, deprecated (active by default).", + ValidateFunc: validation.StringInSlice([]string{"active", + "reserved", "deprecated", "dhcp", "slaac"}, false), + Description: "The status among of this IP address (ipam module) active, reserved, deprecated, dhcp, slaac (active by default).", }, "tag": &tag.TagSchema, "tenant_id": { @@ -123,6 +145,11 @@ func ResourceNetboxIpamIPAddresses() *schema.Resource { Optional: true, Description: "ID of the tenant where this object is attached.", }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The link to this tag (extra module).", + }, "vrf_id": { Type: schema.TypeInt, Optional: true, @@ -132,6 +159,13 @@ func ResourceNetboxIpamIPAddresses() *schema.Resource { } } +var ipAddressRequiredFields = []string{ + "address", + "created", + "last_updated", + "tags", +} + func resourceNetboxIpamIPAddressesCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { client := m.(*netboxclient.NetBoxAPI) @@ -168,14 +202,9 @@ func resourceNetboxIpamIPAddressesCreate(ctx context.Context, d *schema.Resource customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(nil, resourceCustomFields) description := d.Get("description").(string) dnsName := d.Get("dns_name").(string) - natInsideID := int64(d.Get("nat_inside_id").(int)) - objectID := int64(d.Get("object_id").(int)) - objectType := d.Get("object_type").(string) role := d.Get("role").(string) status := d.Get("status").(string) tags := d.Get("tag").(*schema.Set).List() - tenantID := int64(d.Get("tenant_id").(int)) - vrfID := int64(d.Get("vrf_id").(int)) newResource := &models.WritableIPAddress{ Address: &address, @@ -187,20 +216,24 @@ func resourceNetboxIpamIPAddressesCreate(ctx context.Context, d *schema.Resource Tags: tag.ConvertTagsToNestedTags(tags), } - if natInsideID != 0 { + if natInsideID := int64(d.Get("nat_inside_id").(int)); natInsideID != 0 { newResource.NatInside = &natInsideID } - if objectID != 0 { + objectID := int64(0) + objectType := "" + if d.Get("object_id").(int) != 0 { + objectID = int64(d.Get("object_id").(int)) + objectType = d.Get("object_type").(string) newResource.AssignedObjectID = &objectID newResource.AssignedObjectType = &objectType } - if tenantID != 0 { + if tenantID := int64(d.Get("tenant_id").(int)); tenantID != 0 { newResource.Tenant = &tenantID } - if vrfID != 0 { + if vrfID := int64(d.Get("vrf_id").(int)); vrfID != 0 { newResource.Vrf = &vrfID } @@ -224,12 +257,7 @@ func resourceNetboxIpamIPAddressesCreate(ctx context.Context, d *schema.Resource d.SetId(strconv.FormatInt(*addressid, 10)) if primaryIP := d.Get("primary_ip4").(bool); primaryIP { - vmID, err := getVMIDForInterface(client, objectID) - if err != nil { - return diag.FromErr(err) - } - err = updatePrimaryStatus(client, vmID, *addressid, primaryIP) - if err != nil { + if err := setPrimaryIP(m, *addressid, objectID, objectType, true); err != nil { return diag.FromErr(err) } } @@ -248,125 +276,87 @@ func resourceNetboxIpamIPAddressesRead(ctx context.Context, d *schema.ResourceDa return diag.FromErr(err) } - for _, resource := range resources.Payload.Results { - if strconv.FormatInt(resource.ID, 10) == d.Id() { - if err = d.Set("address", resource.Address); err != nil { - return diag.FromErr(err) - } - - if err = d.Set("content_type", util.ConvertURIContentType(resource.URL)); 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) - } - - var description interface{} - if resource.Description == "" { - description = nil - } else { - description = resource.Description - } - - if err = d.Set("description", description); err != nil { - return diag.FromErr(err) - } - - var dnsName interface{} - if resource.DNSName == "" { - dnsName = nil - } else { - dnsName = resource.DNSName - } - - if err = d.Set("dns_name", dnsName); err != nil { - return diag.FromErr(err) - } + if len(resources.Payload.Results) != 1 { + d.SetId("") + return nil + } - var natInsideID *int64 - natInsideID = nil - if resource.NatInside != nil { - natInsideID = &resource.NatInside.ID - } + resource := resources.Payload.Results[0] - if err = d.Set("nat_inside_id", natInsideID); 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("address", resource.Address); err != nil { + return diag.FromErr(err) + } - isPrimary, err := isprimary(m, resource.AssignedObjectID, resource.ID, (*resource.Family.Value == 4)) - if err != nil { - return diag.FromErr(err) - } - if err = d.Set("primary_ip4", isPrimary); err != nil { - return diag.FromErr(err) - } + objectType, objectID := util.GetIPAddressAssignedObject(resource) + if err = d.Set("object_id", objectID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("object_type", objectType); err != nil { + return diag.FromErr(err) + } - if err = d.Set("object_id", resource.AssignedObjectID); err != nil { - return diag.FromErr(err) - } + if err = d.Set("created", resource.Created.String()); err != nil { + return diag.FromErr(err) + } - objectType := resource.AssignedObjectType - if objectType != nil { - *objectType = vMInterfaceType - - if err = d.Set("object_type", *objectType); err != nil { - return diag.FromErr(err) - } - } else { - if err = d.Set("object_type", nil); err != nil { - return diag.FromErr(err) - } - } + resourceCustomFields := d.Get("custom_field").(*schema.Set).List() + customFields := customfield.UpdateCustomFieldsFromAPI(resourceCustomFields, resource.CustomFields) - var roleValue *string - roleValue = nil - if resource.Role != nil { - roleValue = resource.Role.Value - } - if err = d.Set("role", roleValue); err != nil { - return diag.FromErr(err) - } + if err = d.Set("custom_field", customFields); err != nil { + return diag.FromErr(err) + } - var resourceStatus *string - resourceStatus = nil - if resource.Status != nil { - resourceStatus = resource.Status.Value - } - if err = d.Set("status", resourceStatus); err != nil { - return diag.FromErr(err) - } + if err = d.Set("description", resource.Description); err != nil { + return diag.FromErr(err) + } + if err = d.Set("dns_name", resource.DNSName); err != nil { + return diag.FromErr(err) + } + if err = d.Set("family", util.GetIPAddressFamilyLabel(resource.Family)); 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("nat_inside_id", util.GetNestedIPAddressID(resource.NatInside)); err != nil { + return diag.FromErr(err) + } + // if err = d.Set("nat_outside", util.ConvertNestedIPsToIPs(resource.NatOutside)); err != nil { + // return diag.FromErr(err) + // } - if err = d.Set("tag", tag.ConvertNestedTagsToTags(resource.Tags)); err != nil { - return diag.FromErr(err) - } + isPrimary, err := isprimary(m, resource.AssignedObjectID, resource.ID, (*resource.Family.Value == 4)) + if err != nil { + return diag.FromErr(err) + } + if err = d.Set("primary_ip4", isPrimary); err != nil { + return diag.FromErr(err) + } - var tenantID *int64 - tenantID = nil - if resource.Tenant != nil { - tenantID = &resource.Tenant.ID - } - if err = d.Set("tenant_id", tenantID); err != nil { - return diag.FromErr(err) - } + if err = d.Set("role", util.GetIPAddressRoleValue(resource.Role)); err != nil { + return diag.FromErr(err) + } - var vrfID *int64 - vrfID = nil - if resource.Vrf != nil { - vrfID = &resource.Vrf.ID - } - if err = d.Set("vrf_id", vrfID); err != nil { - return diag.FromErr(err) - } + if err = d.Set("status", util.GetIPAddressStatusValue(resource.Status)); err != nil { + return diag.FromErr(err) + } - return nil - } + if err = d.Set("tag", tag.ConvertNestedTagsToTags(resource.Tags)); err != nil { + return diag.FromErr(err) } - d.SetId("") + if err = d.Set("tenant_id", util.GetNestedTenantID(resource.Tenant)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("url", resource.URL); err != nil { + return diag.FromErr(err) + } + if err = d.Set("vrf_id", util.GetNestedVrfID(resource.Vrf)); err != nil { + return diag.FromErr(err) + } return nil } @@ -375,12 +365,21 @@ func resourceNetboxIpamIPAddressesUpdate(ctx context.Context, d *schema.Resource m interface{}) diag.Diagnostics { client := m.(*netboxclient.NetBoxAPI) params := &models.WritableIPAddress{} - // primary_ip4 := false + modifiedFields := map[string]interface{}{} // Required parameters - address := d.Get("address").(string) - params.Address = &address - + if d.HasChange("address") { + address := d.Get("address").(string) + params.Address = &address + } + if d.HasChange("object_id") || d.HasChange("object_type") { + objectID := int64(d.Get("object_id").(int)) + objectType := d.Get("object_type").(string) + params.AssignedObjectID = &objectID + params.AssignedObjectType = &objectType + modifiedFields["assigned_object_id"] = objectID + modifiedFields["assigned_object_type"] = objectType + } if d.HasChange("custom_field") { stateCustomFields, resourceCustomFields := d.GetChange("custom_field") customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(stateCustomFields.(*schema.Set).List(), resourceCustomFields.(*schema.Set).List()) @@ -388,67 +387,41 @@ func resourceNetboxIpamIPAddressesUpdate(ctx context.Context, d *schema.Resource } if d.HasChange("description") { - if description, exist := d.GetOk("description"); exist { - params.Description = description.(string) - } else { - params.Description = " " - } + params.Description = d.Get("description").(string) + modifiedFields["description"] = params.Description } - if d.HasChange("dns_name") { - if dnsName, exist := d.GetOk("dns_name"); exist { - params.DNSName = dnsName.(string) - } else { - params.DNSName = " " - } + params.DNSName = d.Get("dns_name").(string) + modifiedFields["dns_name"] = params.DNSName } - if d.HasChange("nat_inside_id") { natInsideID := int64(d.Get("nat_inside_id").(int)) - if natInsideID != 0 { - params.NatInside = &natInsideID - } + params.NatInside = &natInsideID + modifiedFields["nat_inside"] = natInsideID } - - if d.HasChange("object_id") || d.HasChange("object_type") { - // primary_ip4 = true - objectID := int64(d.Get("object_id").(int)) - params.AssignedObjectID = &objectID - - var objectType string - if *params.AssignedObjectType == "" { - objectType = vMInterfaceType - } else { - objectType = d.Get("object_type").(string) - } - *params.AssignedObjectType = objectType - } - if d.HasChange("role") { role := d.Get("role").(string) params.Role = role + modifiedFields["role"] = role } - if d.HasChange("status") { status := d.Get("status").(string) params.Status = status + modifiedFields["status"] = status + } + if d.HasChange("tag") { + tags := d.Get("tag").(*schema.Set).List() + params.Tags = tag.ConvertTagsToNestedTags(tags) } - - tags := d.Get("tag").(*schema.Set).List() - params.Tags = tag.ConvertTagsToNestedTags(tags) - if d.HasChange("tenant_id") { tenantID := int64(d.Get("tenant_id").(int)) - if tenantID != 0 { - params.Tenant = &tenantID - } + params.Tenant = &tenantID + modifiedFields["tenant"] = tenantID } - if d.HasChange("vrf_id") { vrfID := int64(d.Get("vrf_id").(int)) - if vrfID != 0 { - params.Vrf = &vrfID - } + params.Vrf = &vrfID + modifiedFields["vrf"] = vrfID } resource := ipam.NewIpamIPAddressesPartialUpdateParams().WithData( @@ -461,21 +434,22 @@ func resourceNetboxIpamIPAddressesUpdate(ctx context.Context, d *schema.Resource resource.SetID(resourceID) - _, err = client.Ipam.IpamIPAddressesPartialUpdate(resource, nil) + _, err = client.Ipam.IpamIPAddressesPartialUpdate(resource, nil, requestmodifier.NewNetboxRequestModifier(modifiedFields, ipAddressRequiredFields)) if err != nil { return diag.FromErr(err) } if !d.GetRawConfig().GetAttr("primary_ip4").IsNull() { - if (d.HasChange("object_id") && d.Get("primary_ip4").(bool)) || - (!d.HasChange("object_id") && d.HasChange("primary_ip4")) || - (d.HasChange("primary_ip4") && d.Get("primary_ip4").(bool)) { + objectChanged := d.HasChange("object_id") || d.HasChange("object_type") + primaryIP4 := d.Get("primary_ip4").(bool) + + if (objectChanged && primaryIP4) || + (!objectChanged && d.HasChange("primary_ip4")) || + (d.HasChange("primary_ip4") && primaryIP4) { objectID := int64(d.Get("object_id").(int)) - vmID, err := getVMIDForInterface(client, objectID) - if err != nil { - return diag.FromErr(err) - } - err = updatePrimaryStatus(client, vmID, resourceID, d.Get("primary_ip4").(bool)) + objectType := d.Get("object_type").(string) + + err = setPrimaryIP(client, resourceID, objectID, objectType, primaryIP4) if err != nil { return diag.FromErr(err) } diff --git a/netbox/ipam/resource_netbox_ipam_ip_addresses_test.go b/netbox/ipam/resource_netbox_ipam_ip_addresses_test.go new file mode 100644 index 000000000..6ceadb0ce --- /dev/null +++ b/netbox/ipam/resource_netbox_ipam_ip_addresses_test.go @@ -0,0 +1,168 @@ +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 resourceNameIPAddress = "netbox_ipam_ip_addresses.test" + +func TestAccNetboxIpamIPAddressesMinimal(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + ipNum := int64(acctest.RandIntRange(1, 16384)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxIpamIPAddressConfig(nameSuffix, false, false, ipNum), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIPAddress), + ), + }, + { + ResourceName: resourceNameIPAddress, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxIpamIPAddressesFull(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + ipNum := int64(acctest.RandIntRange(1, 16384)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxIpamIPAddressConfig(nameSuffix, true, true, ipNum), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIPAddress), + ), + }, + { + ResourceName: resourceNameIPAddress, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxIpamIPAddressesMinimalFullMinimal(t *testing.T) { + nameSuffix := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + ipNum := int64(acctest.RandIntRange(1, 16384)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { util.TestAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckNetboxIpamIPAddressConfig(nameSuffix, false, false, ipNum), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIPAddress), + ), + }, + { + Config: testAccCheckNetboxIpamIPAddressConfig(nameSuffix, true, true, ipNum), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIPAddress), + ), + }, + { + Config: testAccCheckNetboxIpamIPAddressConfig(nameSuffix, false, true, ipNum), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIPAddress), + ), + }, + { + Config: testAccCheckNetboxIpamIPAddressConfig(nameSuffix, false, false, ipNum), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIPAddress), + ), + }, + }, + }) +} + +func testAccCheckNetboxIpamIPAddressConfig(nameSuffix string, resourceFull, extraResources bool, ipNum int64) string { + const template = ` + {{ if eq .extraresources "true" }} + 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 + } + + resource "netbox_virtualization_interface" "test" { + name = "test-{{ .namesuffix }}" + virtualmachine_id = netbox_virtualization_vm.test.id + } + + resource "netbox_extras_tag" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_tenancy_tenant" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + } + + resource "netbox_ipam_ip_addresses" "nat" { + address = "${cidrhost("10.0.0.0/8", {{ .ipnum }} + 2 )}/24" + } + + #resource "netbox_ipam_vrf" "test" { + # name = "test-{{ .namesuffix }}" + #} + {{ end }} + + resource "netbox_ipam_ip_addresses" "test" { + address = "${cidrhost("10.0.0.0/8", {{ .ipnum }})}/24" + + {{ if eq .resourcefull "true" }} + description = "Test ip address" + dns_name = "test.example.local" + role = "vip" + status = "reserved" + # vrf_id = netbox_ipam_vrf.test.id + vrf_id = 1 + tenant_id = netbox_tenancy_tenant.test.id + nat_inside_id = netbox_ipam_ip_addresses.nat.id + + object_id = netbox_virtualization_interface.test.id + object_type = "virtualization.vminterface" + + 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), + "ipnum": strconv.FormatInt(ipNum, 10), + } + return util.RenderTemplate(template, data) +} diff --git a/netbox/ipam/util.go b/netbox/ipam/util.go index 8128b2741..81888b95e 100644 --- a/netbox/ipam/util.go +++ b/netbox/ipam/util.go @@ -13,6 +13,8 @@ import ( // Type of vm interface in Netbox const vMInterfaceType string = "virtualization.vminterface" +const deviceInterfaceType string = "dcim.interface" +const fhrpgroupType string = "ipam.fhrpgroup" func getNewAvailableIPForIPRange(client *netboxclient.NetBoxAPI, id int64) (*models.IPAddress, error) { params := ipam.NewIpamIPRangesAvailableIpsCreateParams().WithID(id) @@ -124,6 +126,29 @@ func isprimary(m interface{}, objectID *int64, ipID int64, ip4 bool) (bool, erro return false, nil } +func setPrimaryIP(m interface{}, addressID, objectID int64, objectType string, primary bool) error { + client := m.(*netboxclient.NetBoxAPI) + + switch objectType { + case vMInterfaceType: + vmID, err := getVMIDForInterface(client, objectID) + if err != nil { + return err + } + err = updatePrimaryStatus(client, vmID, addressID, primary) + if err != nil { + return err + } + return nil + case deviceInterfaceType: + return fmt.Errorf("this provider does not support the primary_ip4 attribute for '%s'", deviceInterfaceType) + case fhrpgroupType: + return fmt.Errorf("netbox does not support the primary_ip4 attribute for '%s'", fhrpgroupType) + default: + return fmt.Errorf("unknown object type '%s'", objectType) + } +} + func updatePrimaryStatus(m interface{}, vmid, ipid int64, primary bool) error { client := m.(*netboxclient.NetBoxAPI) From 00e2c2231a33c10412d65d84d6a761ae999bdeb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 12 Mar 2023 12:25:56 +0100 Subject: [PATCH 13/13] docs: Add import documentation --- examples/resources/netbox_dcim_device_role/import.sh | 2 ++ examples/resources/netbox_dcim_manufacturer/import.sh | 2 ++ examples/resources/netbox_dcim_platform/import.sh | 2 ++ examples/resources/netbox_extras_tag/import.sh | 2 ++ examples/resources/netbox_ipam_aggregate/import.sh | 2 ++ examples/resources/netbox_ipam_asn/import.sh | 2 ++ examples/resources/netbox_ipam_asn/resource.tf | 2 +- examples/resources/netbox_ipam_ip_range/import.sh | 2 ++ examples/resources/netbox_ipam_prefix/import.sh | 2 ++ examples/resources/netbox_ipam_service/import.sh | 2 ++ examples/resources/netbox_ipam_vlan/import.sh | 2 ++ examples/resources/netbox_tenancy_contact/import.sh | 2 ++ .../resources/netbox_tenancy_contact_assignment/import.sh | 2 ++ examples/resources/netbox_tenancy_contact_group/import.sh | 2 ++ examples/resources/netbox_tenancy_contact_role/import.sh | 2 ++ examples/resources/netbox_tenancy_tenant/import.sh | 2 ++ examples/resources/netbox_tenancy_tenant_group/import.sh | 2 ++ .../resources/netbox_virtualization_cluster_group/import.sh | 2 ++ .../resources/netbox_virtualization_cluster_group/resource.tf | 4 ++-- .../resources/netbox_virtualization_cluster_type/import.sh | 2 ++ .../resources/netbox_virtualization_cluster_type/resource.tf | 4 ++-- 21 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 examples/resources/netbox_dcim_device_role/import.sh create mode 100644 examples/resources/netbox_dcim_manufacturer/import.sh create mode 100644 examples/resources/netbox_dcim_platform/import.sh create mode 100644 examples/resources/netbox_extras_tag/import.sh create mode 100644 examples/resources/netbox_ipam_aggregate/import.sh create mode 100644 examples/resources/netbox_ipam_asn/import.sh create mode 100644 examples/resources/netbox_ipam_ip_range/import.sh create mode 100644 examples/resources/netbox_ipam_prefix/import.sh create mode 100644 examples/resources/netbox_ipam_service/import.sh create mode 100644 examples/resources/netbox_ipam_vlan/import.sh create mode 100644 examples/resources/netbox_tenancy_contact/import.sh create mode 100644 examples/resources/netbox_tenancy_contact_assignment/import.sh create mode 100644 examples/resources/netbox_tenancy_contact_group/import.sh create mode 100644 examples/resources/netbox_tenancy_contact_role/import.sh create mode 100644 examples/resources/netbox_tenancy_tenant/import.sh create mode 100644 examples/resources/netbox_tenancy_tenant_group/import.sh create mode 100644 examples/resources/netbox_virtualization_cluster_group/import.sh create mode 100644 examples/resources/netbox_virtualization_cluster_type/import.sh diff --git a/examples/resources/netbox_dcim_device_role/import.sh b/examples/resources/netbox_dcim_device_role/import.sh new file mode 100644 index 000000000..1cf3b32e4 --- /dev/null +++ b/examples/resources/netbox_dcim_device_role/import.sh @@ -0,0 +1,2 @@ +# Device roles can be imported by id +terraform import netbox_dcim_device_role.device_role_test 1 diff --git a/examples/resources/netbox_dcim_manufacturer/import.sh b/examples/resources/netbox_dcim_manufacturer/import.sh new file mode 100644 index 000000000..d7658ed05 --- /dev/null +++ b/examples/resources/netbox_dcim_manufacturer/import.sh @@ -0,0 +1,2 @@ +# Manufacturers can be imported by id +terraform import netbox_dcim_manufacturer.manufacturer_test 1 diff --git a/examples/resources/netbox_dcim_platform/import.sh b/examples/resources/netbox_dcim_platform/import.sh new file mode 100644 index 000000000..55b46d9e4 --- /dev/null +++ b/examples/resources/netbox_dcim_platform/import.sh @@ -0,0 +1,2 @@ +# Platforms can be imported by id +terraform import netbox_dcim_platform.platform_test 1 diff --git a/examples/resources/netbox_extras_tag/import.sh b/examples/resources/netbox_extras_tag/import.sh new file mode 100644 index 000000000..e9dd39df9 --- /dev/null +++ b/examples/resources/netbox_extras_tag/import.sh @@ -0,0 +1,2 @@ +# Tags can be imported by id +terraform import netbox_extras_tag.tag_test 1 diff --git a/examples/resources/netbox_ipam_aggregate/import.sh b/examples/resources/netbox_ipam_aggregate/import.sh new file mode 100644 index 000000000..fac81382c --- /dev/null +++ b/examples/resources/netbox_ipam_aggregate/import.sh @@ -0,0 +1,2 @@ +# Aggregates can be imported by id +terraform import netbox_ipam_aggregate.aggregate_test 1 diff --git a/examples/resources/netbox_ipam_asn/import.sh b/examples/resources/netbox_ipam_asn/import.sh new file mode 100644 index 000000000..bed833981 --- /dev/null +++ b/examples/resources/netbox_ipam_asn/import.sh @@ -0,0 +1,2 @@ +# ASNs can be imported by id +terraform import netbox_ipam_asn.asn_test 1 diff --git a/examples/resources/netbox_ipam_asn/resource.tf b/examples/resources/netbox_ipam_asn/resource.tf index 337f36fd6..8d9f77889 100644 --- a/examples/resources/netbox_ipam_asn/resource.tf +++ b/examples/resources/netbox_ipam_asn/resource.tf @@ -1,4 +1,4 @@ -resource "netbox_ipam_aggregate" "aggregate_test" { +resource "netbox_ipam_asn" "asn_test" { asn = "65530" rir_id = netbox_ipam_rir.rir_test.id diff --git a/examples/resources/netbox_ipam_ip_range/import.sh b/examples/resources/netbox_ipam_ip_range/import.sh new file mode 100644 index 000000000..2b8ae0fc6 --- /dev/null +++ b/examples/resources/netbox_ipam_ip_range/import.sh @@ -0,0 +1,2 @@ +# IP ranges can be imported by id +terraform import netbox_ipam_ip_range.range_test 1 diff --git a/examples/resources/netbox_ipam_prefix/import.sh b/examples/resources/netbox_ipam_prefix/import.sh new file mode 100644 index 000000000..0702699ee --- /dev/null +++ b/examples/resources/netbox_ipam_prefix/import.sh @@ -0,0 +1,2 @@ +# Prefixes can be imported by id +terraform import netbox_ipam_prefix.prefix_test 1 diff --git a/examples/resources/netbox_ipam_service/import.sh b/examples/resources/netbox_ipam_service/import.sh new file mode 100644 index 000000000..2b5d14266 --- /dev/null +++ b/examples/resources/netbox_ipam_service/import.sh @@ -0,0 +1,2 @@ +# Services can be imported by id +terraform import netbox_ipam_service.service_test 1 diff --git a/examples/resources/netbox_ipam_vlan/import.sh b/examples/resources/netbox_ipam_vlan/import.sh new file mode 100644 index 000000000..621eb436b --- /dev/null +++ b/examples/resources/netbox_ipam_vlan/import.sh @@ -0,0 +1,2 @@ +# VLANs can be imported by id +terraform import netbox_ipam_vlan.vlan_test 1 diff --git a/examples/resources/netbox_tenancy_contact/import.sh b/examples/resources/netbox_tenancy_contact/import.sh new file mode 100644 index 000000000..0e94a7204 --- /dev/null +++ b/examples/resources/netbox_tenancy_contact/import.sh @@ -0,0 +1,2 @@ +# Contacts can be imported by id +terraform import netbox_tenancy_contact.contact_test 1 diff --git a/examples/resources/netbox_tenancy_contact_assignment/import.sh b/examples/resources/netbox_tenancy_contact_assignment/import.sh new file mode 100644 index 000000000..d7b8a3493 --- /dev/null +++ b/examples/resources/netbox_tenancy_contact_assignment/import.sh @@ -0,0 +1,2 @@ +# Contact assignments can be imported by id +terraform import netbox_tenancy_contact_assignment.contact_assignment_01 1 diff --git a/examples/resources/netbox_tenancy_contact_group/import.sh b/examples/resources/netbox_tenancy_contact_group/import.sh new file mode 100644 index 000000000..2bdfe79a9 --- /dev/null +++ b/examples/resources/netbox_tenancy_contact_group/import.sh @@ -0,0 +1,2 @@ +# Contact groups roles can be imported by id +terraform import netbox_tenancy_contact_group.contact_group_test 1 diff --git a/examples/resources/netbox_tenancy_contact_role/import.sh b/examples/resources/netbox_tenancy_contact_role/import.sh new file mode 100644 index 000000000..ef85c2aaa --- /dev/null +++ b/examples/resources/netbox_tenancy_contact_role/import.sh @@ -0,0 +1,2 @@ +# Contact roles can be imported by id +terraform import netbox_tenancy_contact_role.contact_role_test 1 diff --git a/examples/resources/netbox_tenancy_tenant/import.sh b/examples/resources/netbox_tenancy_tenant/import.sh new file mode 100644 index 000000000..d2c94a1f7 --- /dev/null +++ b/examples/resources/netbox_tenancy_tenant/import.sh @@ -0,0 +1,2 @@ +# Tenants can be imported by id +terraform import netbox_tenancy_tenant.tenant_test 1 diff --git a/examples/resources/netbox_tenancy_tenant_group/import.sh b/examples/resources/netbox_tenancy_tenant_group/import.sh new file mode 100644 index 000000000..5d169d6b6 --- /dev/null +++ b/examples/resources/netbox_tenancy_tenant_group/import.sh @@ -0,0 +1,2 @@ +# Tenant groups can be imported by id +terraform import netbox_tenancy_tenant_group.tenant_group_test 1 diff --git a/examples/resources/netbox_virtualization_cluster_group/import.sh b/examples/resources/netbox_virtualization_cluster_group/import.sh new file mode 100644 index 000000000..1222bfb3a --- /dev/null +++ b/examples/resources/netbox_virtualization_cluster_group/import.sh @@ -0,0 +1,2 @@ +# Cluster groups can be imported by id +terraform import netbox_virtualization_cluster_group.test 1 diff --git a/examples/resources/netbox_virtualization_cluster_group/resource.tf b/examples/resources/netbox_virtualization_cluster_group/resource.tf index b4971ee5d..ffb1f90ac 100644 --- a/examples/resources/netbox_virtualization_cluster_group/resource.tf +++ b/examples/resources/netbox_virtualization_cluster_group/resource.tf @@ -1,7 +1,7 @@ -resource "netbox_virtualization_cluster_type" "test" { +resource "netbox_virtualization_cluster_group" "test" { name = "test-{{ .namesuffix }}" slug = "test-{{ .namesuffix }}" - description = "Test device role" + description = "Test cluster group" tag { name = "tag1" diff --git a/examples/resources/netbox_virtualization_cluster_type/import.sh b/examples/resources/netbox_virtualization_cluster_type/import.sh new file mode 100644 index 000000000..ce7fd62cb --- /dev/null +++ b/examples/resources/netbox_virtualization_cluster_type/import.sh @@ -0,0 +1,2 @@ +# Cluster types can be imported by id +terraform import netbox_virtualization_cluster_type.test 1 diff --git a/examples/resources/netbox_virtualization_cluster_type/resource.tf b/examples/resources/netbox_virtualization_cluster_type/resource.tf index 298fc1893..2885a3d21 100644 --- a/examples/resources/netbox_virtualization_cluster_type/resource.tf +++ b/examples/resources/netbox_virtualization_cluster_type/resource.tf @@ -1,7 +1,7 @@ -resource "netbox_virtualization_cluster_group" "test" { +resource "netbox_virtualization_cluster_type" "test" { name = "test-{{ .namesuffix }}" slug = "test-{{ .namesuffix }}" - description = "Test device role" + description = "Test cluster type" tag { name = "tag1"