From 2aa2edd8e84d8cdc3969dfadf8661780fbd71adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nie=C3=9F?= Date: Sun, 30 Oct 2022 21:52:57 +0100 Subject: [PATCH] feat: Add ipam rir resource --- examples/main.tf | 13 +- .../netbox_ipam_aggregate/resource.tf | 2 +- .../resources/netbox_ipam_rir/resource.tf | 83 ++++++ .../requestmodifier/netboxrequestmodifier.go | 80 +++++ .../resource_netbox_ipam_aggregate_test.go | 12 +- netbox/ipam/resource_netbox_ipam_rir.go | 281 ++++++++++++++++++ netbox/ipam/resource_netbox_ipam_rir_test.go | 123 ++++++++ netbox/provider.go | 1 + 8 files changed, 581 insertions(+), 14 deletions(-) create mode 100644 examples/resources/netbox_ipam_rir/resource.tf create mode 100644 netbox/internal/requestmodifier/netboxrequestmodifier.go create mode 100644 netbox/ipam/resource_netbox_ipam_rir.go create mode 100644 netbox/ipam/resource_netbox_ipam_rir_test.go diff --git a/examples/main.tf b/examples/main.tf index c3f58aea3..6fd383972 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -628,6 +628,13 @@ resource "netbox_virtualization_vm" "vm_test" { } } +resource "netbox_ipam_rir" "rir_test" { + name = "Test RIR" + slug = "test-rir" + + description = "Test RIR" +} + resource "netbox_ipam_service" "service_test" { name = "SMTP" virtualmachine_id = netbox_virtualization_vm.vm_test.id @@ -721,13 +728,9 @@ resource "netbox_virtualization_interface" "interface_test" { description = "Interface de test" } -data "netbox_json_ipam_rirs_list" "json_rir" { - limit = 1 -} - resource "netbox_ipam_aggregate" "aggregate_test" { prefix = "192.167.0.0/24" - rir_id = jsondecode(data.netbox_json_ipam_rirs_list.json_rir.json)[0].id + rir_id = netbox_ipam_rir.rir_test.id date_added = "2020-12-21" description = "Aggregate created by terraform" diff --git a/examples/resources/netbox_ipam_aggregate/resource.tf b/examples/resources/netbox_ipam_aggregate/resource.tf index 718abd6d7..d98858a7b 100644 --- a/examples/resources/netbox_ipam_aggregate/resource.tf +++ b/examples/resources/netbox_ipam_aggregate/resource.tf @@ -1,6 +1,6 @@ resource "netbox_ipam_aggregate" "aggregate_test" { prefix = "192.168.56.0/24" - rir_id = 1 + rir_id = netbox_ipam_rir.rir_test.id date_created = "2020-12-21" tag { diff --git a/examples/resources/netbox_ipam_rir/resource.tf b/examples/resources/netbox_ipam_rir/resource.tf new file mode 100644 index 000000000..5926cd2be --- /dev/null +++ b/examples/resources/netbox_ipam_rir/resource.tf @@ -0,0 +1,83 @@ +resource "netbox_ipam_rir" "rir_test" { + name = "Test RIR" + slug = "test-rir" + + description = "Test RIR" + + tag { + name = "tag1" + slug = "tag1" + } + + custom_field { + name = "cf_boolean" + type = "boolean" + value = "true" + } + + custom_field { + name = "cf_date" + type = "date" + value = "2020-12-25" + } + + custom_field { + name = "cf_text" + type = "text" + value = "some text" + } + + custom_field { + name = "cf_integer" + type = "integer" + value = "10" + } + + custom_field { + name = "cf_selection" + type = "select" + value = "1" + } + + custom_field { + name = "cf_url" + type = "url" + value = "https://github.com" + } + + custom_field { + name = "cf_multi_selection" + type = "multiselect" + value = jsonencode([ + "0", + "1" + ]) + } + + custom_field { + name = "cf_json" + type = "json" + value = jsonencode({ + stringvalue = "string" + boolvalue = false + dictionary = { + numbervalue = 5 + } + }) + } + + custom_field { + name = "cf_object" + type = "object" + value = 1 + } + + custom_field { + name = "cf_multi_object" + type = "multiobject" + value = jsonencode([ + 1, + 2 + ]) + } +} diff --git a/netbox/internal/requestmodifier/netboxrequestmodifier.go b/netbox/internal/requestmodifier/netboxrequestmodifier.go new file mode 100644 index 000000000..33ac49f7b --- /dev/null +++ b/netbox/internal/requestmodifier/netboxrequestmodifier.go @@ -0,0 +1,80 @@ +package requestmodifier + +import ( + "encoding/json" + "fmt" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" +) + +type netboxRequestModifier struct { + origwriter runtime.ClientRequestWriter + modifiedFields map[string]interface{} + requiredFields []string +} + +func (o netboxRequestModifier) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + err := o.origwriter.WriteToRequest(r, reg) + if err != nil { + return err + } + + jsonString, err := json.Marshal(r.GetBodyParam()) + if err != nil { + return err + } + + var objmap map[string]interface{} + err = json.Unmarshal(jsonString, &objmap) + if err != nil { + return err + } + + for _, k := range o.requiredFields { + if val, ok := objmap[k]; ok && (val == nil || val == "0001-01-01T00:00:00.000Z") { + delete(objmap, k) + } + } + + for k, v := range o.modifiedFields { + switch v.(type) { + case int64: + if v == int64(0) { + objmap[k] = nil + } + case float64: + if v == float64(0) { + objmap[k] = nil + } + case bool: + if v == false { + objmap[k] = false + } + case string: + if v == "" { + objmap[k] = "" + } + case nil: + objmap[k] = nil + default: + return fmt.Errorf("unknown type for '%s'", k) + } + } + + err = r.SetBodyParam(objmap) + return err +} + +func NewNetboxRequestModifier(modifiedFields map[string]interface{}, requiredFields []string) func(*runtime.ClientOperation) { + return func(op *runtime.ClientOperation) { + if len(modifiedFields) > 0 || len(requiredFields) > 0 { + tmp := netboxRequestModifier{ + origwriter: op.Params, + modifiedFields: modifiedFields, + requiredFields: requiredFields, + } + op.Params = tmp + } + } +} diff --git a/netbox/ipam/resource_netbox_ipam_aggregate_test.go b/netbox/ipam/resource_netbox_ipam_aggregate_test.go index ba110d680..8b46c6601 100644 --- a/netbox/ipam/resource_netbox_ipam_aggregate_test.go +++ b/netbox/ipam/resource_netbox_ipam_aggregate_test.go @@ -95,12 +95,9 @@ func TestAccNetboxIpamAggregateMininmalFullMinimal(t *testing.T) { func testAccCheckNetboxIPAMAggregateConfig(nameSuffix string, resourceFull, extraResources bool, prefix string) string { template := ` - #resource "netbox_ipam_rir" "test" { - # name = "test-{{ .namesuffix }}" - # slug = "test-{{ .namesuffix }}" - #} - data "netbox_json_ipam_rirs_list" "json_rir" { - limit = 1 + resource "netbox_ipam_rir" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" } {{ if eq .extraresources "true" }} @@ -117,8 +114,7 @@ func testAccCheckNetboxIPAMAggregateConfig(nameSuffix string, resourceFull, extr resource "netbox_ipam_aggregate" "test" { prefix = "{{ .prefix }}" - rir_id = jsondecode(data.netbox_json_ipam_rirs_list.json_rir.json)[0].id - #rir_id = netbox_ipam_rir.test.id + rir_id = netbox_ipam_rir.test.id {{ if eq .resourcefull "true" }} tenant_id = netbox_tenancy_tenant.test.id diff --git a/netbox/ipam/resource_netbox_ipam_rir.go b/netbox/ipam/resource_netbox_ipam_rir.go new file mode 100644 index 000000000..56e4d24de --- /dev/null +++ b/netbox/ipam/resource_netbox_ipam_rir.go @@ -0,0 +1,281 @@ +package ipam + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + netboxclient "github.com/smutel/go-netbox/v3/netbox/client" + "github.com/smutel/go-netbox/v3/netbox/client/ipam" + "github.com/smutel/go-netbox/v3/netbox/models" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/customfield" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/requestmodifier" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/tag" + "github.com/smutel/terraform-provider-netbox/v4/netbox/internal/util" +) + +func ResourceNetboxIpamRIR() *schema.Resource { + return &schema.Resource{ + Description: "Manage a rir (ipam module) within Netbox.", + CreateContext: resourceNetboxIpamRIRCreate, + ReadContext: resourceNetboxIpamRIRRead, + UpdateContext: resourceNetboxIpamRIRUpdate, + DeleteContext: resourceNetboxIpamRIRDelete, + // Exists: resourceNetboxIpamRIRExists, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "aggregate_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The number of aggregates with this rir (ipam module).", + }, + "content_type": { + Type: schema.TypeString, + Computed: true, + Description: "The content type of this rir (ipam module).", + }, + "created": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this rir was created.", + }, + "custom_field": &customfield.CustomFieldSchema, + "description": { + Type: schema.TypeString, + Optional: true, + Default: nil, + ValidateFunc: validation.StringLenBetween(1, 200), + Description: "The description of this rir (ipam module).", + }, + "is_private": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Date when this rir was created.", + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + Description: "Date when this rir was created.", + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), + Description: "The name of this rir (ipam module).", + }, + "slug": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), + Description: "The slug of this rir (ipam module).", + }, + "tag": &tag.TagSchema, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The link to this rir (ipam module).", + }, + }, + } +} + +var rirRequiredFields = []string{ + "created", + "last_updated", + "name", + "slug", + "tag", +} + +func resourceNetboxIpamRIRCreate(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceCustomFields := d.Get("custom_field").(*schema.Set).List() + customFields := customfield.ConvertCustomFieldsFromTerraformToAPI(nil, resourceCustomFields) + + name := d.Get("name").(string) + slug := d.Get("slug").(string) + tags := d.Get("tag").(*schema.Set).List() + + newResource := &models.RIR{ + CustomFields: &customFields, + Description: d.Get("description").(string), + IsPrivate: d.Get("is_private").(bool), + Name: &name, + Slug: &slug, + Tags: tag.ConvertTagsToNestedTags(tags), + } + + resource := ipam.NewIpamRirsCreateParams().WithData(newResource) + + resourceCreated, err := client.Ipam.IpamRirsCreate(resource, nil) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(resourceCreated.Payload.ID, 10)) + + return resourceNetboxIpamRIRRead(ctx, d, m) +} + +func resourceNetboxIpamRIRRead(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceID := d.Id() + params := ipam.NewIpamRirsListParams().WithID(&resourceID) + resources, err := client.Ipam.IpamRirsList(params, nil) + if err != nil { + return diag.FromErr(err) + } + + if len(resources.Payload.Results) != 1 { + d.SetId("") + return nil + } + + resource := resources.Payload.Results[0] + + if err = d.Set("content_type", util.ConvertURIContentType(resource.URL)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("aggregate_count", resource.AggregateCount); 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("is_private", resource.IsPrivate); err != nil { + return diag.FromErr(err) + } + if err = d.Set("last_updated", resource.LastUpdated.String()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("name", resource.Name); err != nil { + return diag.FromErr(err) + } + if err = d.Set("slug", resource.Slug); err != nil { + return diag.FromErr(err) + } + if err = d.Set("tag", tag.ConvertNestedTagsToTags(resource.Tags)); err != nil { + return diag.FromErr(err) + } + if err = d.Set("url", resource.URL); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceNetboxIpamRIRUpdate(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + modiefiedFields := make(map[string]interface{}) + + resourceID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.Errorf("Unable to convert ID into int64") + } + params := &models.RIR{} + + 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") { + params.Description = d.Get("description").(string) + modiefiedFields["description"] = params.Description + } + if d.HasChange("is_private") { + params.IsPrivate = d.Get("is_private").(bool) + modiefiedFields["is_private"] = params.IsPrivate + } + if d.HasChange("name") { + name := d.Get("name").(string) + params.Name = &name + } + 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.NewIpamRirsPartialUpdateParams().WithData(params) + + resource.SetID(resourceID) + + _, err = client.Ipam.IpamRirsPartialUpdate(resource, nil, requestmodifier.NewNetboxRequestModifier(modiefiedFields, rirRequiredFields)) + if err != nil { + return diag.FromErr(err) + } + + return resourceNetboxIpamRIRRead(ctx, d, m) +} + +func resourceNetboxIpamRIRDelete(ctx context.Context, d *schema.ResourceData, + m interface{}) diag.Diagnostics { + client := m.(*netboxclient.NetBoxAPI) + + resourceExists, err := resourceNetboxIpamRIRExists(d, m) + if err != nil { + return diag.FromErr(err) + } + + if !resourceExists { + return nil + } + + id, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return diag.Errorf("Unable to convert ID into int64") + } + + resource := ipam.NewIpamRirsDeleteParams().WithID(id) + if _, err := client.Ipam.IpamRirsDelete(resource, nil); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceNetboxIpamRIRExists(d *schema.ResourceData, + m interface{}) (b bool, e error) { + client := m.(*netboxclient.NetBoxAPI) + resourceExist := false + + resourceID := d.Id() + params := ipam.NewIpamRirsListParams().WithID(&resourceID) + resources, err := client.Ipam.IpamRirsList(params, nil) + if err != nil { + return resourceExist, err + } + + for _, resource := range resources.Payload.Results { + if strconv.FormatInt(resource.ID, 10) == d.Id() { + resourceExist = true + } + } + + return resourceExist, nil +} diff --git a/netbox/ipam/resource_netbox_ipam_rir_test.go b/netbox/ipam/resource_netbox_ipam_rir_test.go new file mode 100644 index 000000000..74f2719df --- /dev/null +++ b/netbox/ipam/resource_netbox_ipam_rir_test.go @@ -0,0 +1,123 @@ +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/v4/netbox/internal/util" +) + +const resourceNameIpamRIR = "netbox_ipam_rir.test" + +func TestAccNetboxIpamRIRMinimal(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: testAccCheckNetboxIPAMRIRConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIpamRIR), + ), + }, + { + ResourceName: resourceNameIpamRIR, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxIpamRIRFull(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: testAccCheckNetboxIPAMRIRConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIpamRIR), + ), + }, + { + ResourceName: resourceNameIpamRIR, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNetboxIpamRIRMininmalFullMinimal(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: testAccCheckNetboxIPAMRIRConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIpamRIR), + ), + }, + { + Config: testAccCheckNetboxIPAMRIRConfig(nameSuffix, true, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIpamRIR), + ), + }, + { + Config: testAccCheckNetboxIPAMRIRConfig(nameSuffix, false, true), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIpamRIR), + ), + }, + { + Config: testAccCheckNetboxIPAMRIRConfig(nameSuffix, false, false), + Check: resource.ComposeTestCheckFunc( + util.TestAccResourceExists(resourceNameIpamRIR), + ), + }, + }, + }) +} + +func testAccCheckNetboxIPAMRIRConfig(nameSuffix string, resourceFull, extraResources bool) string { + template := ` + {{ if eq .extraresources "true" }} + #resource "netbox_extras_tag" "test" { + # name = "test-{{ .namesuffix }}" + # slug = "test-{{ .namesuffix }}" + #} + {{ end }} + + resource "netbox_ipam_rir" "test" { + name = "test-{{ .namesuffix }}" + slug = "test-{{ .namesuffix }}" + {{ if eq .resourcefull "true" }} + description = "Test RIR" + is_private = true + + #tag { + # name = netbox_extras_tag.test.name + # slug = netbox_extras_tag.test.slug + #} + {{ end }} + } + ` + data := map[string]string{ + "namesuffix": nameSuffix, + "resourcefull": strconv.FormatBool(resourceFull), + "extraresources": strconv.FormatBool(extraResources), + } + return util.RenderTemplate(template, data) +} diff --git a/netbox/provider.go b/netbox/provider.go index dcc832f7c..f6d52e00a 100644 --- a/netbox/provider.go +++ b/netbox/provider.go @@ -165,6 +165,7 @@ func Provider() *schema.Provider { "netbox_ipam_ip_addresses": ipam.ResourceNetboxIpamIPAddresses(), "netbox_ipam_ip_range": ipam.ResourceNetboxIpamIPRange(), "netbox_ipam_prefix": ipam.ResourceNetboxIpamPrefix(), + "netbox_ipam_rir": ipam.ResourceNetboxIpamRIR(), "netbox_ipam_service": ipam.ResourceNetboxIpamService(), "netbox_ipam_vlan": ipam.ResourceNetboxIpamVlan(), "netbox_ipam_vlan_group": ipam.ResourceNetboxIpamVlanGroup(),