From 4d0ce77010390ac5aaa545b0f6bd343077a44df0 Mon Sep 17 00:00:00 2001 From: Sebastiaan Koppe Date: Sun, 2 Jun 2024 15:42:13 +0200 Subject: [PATCH] feat: add 'netbox_ip_address_assignment' resource (#601) --- .../device_interface_id.tf | 16 + .../object_type_device.tf | 17 + .../object_type_virtual_machine.tf | 16 + .../virtual_machine_interface_id.tf | 16 + netbox/provider.go | 1 + .../resource_netbox_available_ip_address.go | 60 +++- ...source_netbox_available_ip_address_test.go | 110 ++++++ netbox/resource_netbox_ip_address.go | 60 +++- .../resource_netbox_ip_address_assignment.go | 211 +++++++++++ ...ource_netbox_ip_address_assignment_test.go | 340 ++++++++++++++++++ netbox/resource_netbox_ip_address_test.go | 175 +++++++++ 11 files changed, 990 insertions(+), 32 deletions(-) create mode 100644 examples/resources/netbox_ip_address_assignment/device_interface_id.tf create mode 100644 examples/resources/netbox_ip_address_assignment/object_type_device.tf create mode 100644 examples/resources/netbox_ip_address_assignment/object_type_virtual_machine.tf create mode 100644 examples/resources/netbox_ip_address_assignment/virtual_machine_interface_id.tf create mode 100644 netbox/resource_netbox_ip_address_assignment.go create mode 100644 netbox/resource_netbox_ip_address_assignment_test.go diff --git a/examples/resources/netbox_ip_address_assignment/device_interface_id.tf b/examples/resources/netbox_ip_address_assignment/device_interface_id.tf new file mode 100644 index 00000000..8b83c08f --- /dev/null +++ b/examples/resources/netbox_ip_address_assignment/device_interface_id.tf @@ -0,0 +1,16 @@ +// Assuming a device with the id `123` exists +resource "netbox_device_interface" "this" { + name = "eth0" + device_id = 123 + type = "1000base-t" +} + +resource "netbox_ip_address" "this" { + ip_address = "10.0.0.60/24" + status = "active" +} + +resource "netbox_ip_address_assignment" "this" { + ip_address_id = netbox_ip_address.this.id + device_interface_id = netbox_device_interface.this.id +} diff --git a/examples/resources/netbox_ip_address_assignment/object_type_device.tf b/examples/resources/netbox_ip_address_assignment/object_type_device.tf new file mode 100644 index 00000000..2abdcebc --- /dev/null +++ b/examples/resources/netbox_ip_address_assignment/object_type_device.tf @@ -0,0 +1,17 @@ +// Assuming a device with the id `123` exists +resource "netbox_device_interface" "this" { + name = "eth0" + device_id = 123 + type = "1000base-t" +} + +resource "netbox_ip_address" "this" { + ip_address = "10.0.0.60/24" + status = "active" +} + +resource "netbox_ip_address_assignment" "this" { + ip_address_id = netbox_ip_address.this.id + interface_id = netbox_device_interface.this.id + object_type = "dcim.interface" +} diff --git a/examples/resources/netbox_ip_address_assignment/object_type_virtual_machine.tf b/examples/resources/netbox_ip_address_assignment/object_type_virtual_machine.tf new file mode 100644 index 00000000..a137ec44 --- /dev/null +++ b/examples/resources/netbox_ip_address_assignment/object_type_virtual_machine.tf @@ -0,0 +1,16 @@ +// Assuming a virtual machine with the id `123` exists +resource "netbox_interface" "this" { + name = "eth0" + virtual_machine_id = 123 +} + +resource "netbox_ip_address" "this" { + ip_address = "10.0.0.60/24" + status = "active" +} + +resource "netbox_ip_address_assignment" "this" { + ip_address_id = netbox_ip_address.this.id + interface_id = netbox_interface.this.id + object_type = "virtualization.vminterface" +} diff --git a/examples/resources/netbox_ip_address_assignment/virtual_machine_interface_id.tf b/examples/resources/netbox_ip_address_assignment/virtual_machine_interface_id.tf new file mode 100644 index 00000000..81880e41 --- /dev/null +++ b/examples/resources/netbox_ip_address_assignment/virtual_machine_interface_id.tf @@ -0,0 +1,16 @@ +// Assuming a virtual machine with the id `123` exists +resource "netbox_interface" "this" { + name = "eth0" + virtual_machine_id = 123 +} + +resource "netbox_ip_address" "this" { + ip_address = "10.0.0.60/24" + status = "active" +} + + +resource "netbox_ip_address_assignment" "this" { + ip_address_id = netbox_ip_address.this.id + virtual_machine_interface_id = netbox_interface.this.id +} diff --git a/netbox/provider.go b/netbox/provider.go index 25f39ec4..1bff24cc 100644 --- a/netbox/provider.go +++ b/netbox/provider.go @@ -89,6 +89,7 @@ func Provider() *schema.Provider { "netbox_tenant_group": resourceNetboxTenantGroup(), "netbox_vrf": resourceNetboxVrf(), "netbox_ip_address": resourceNetboxIPAddress(), + "netbox_ip_address_assignment": resourceNetboxIPAddressAssignment(), "netbox_interface_template": resourceNetboxInterfaceTemplate(), "netbox_interface": resourceNetboxInterface(), "netbox_service": resourceNetboxService(), diff --git a/netbox/resource_netbox_available_ip_address.go b/netbox/resource_netbox_available_ip_address.go index 5e2a1869..47515fc4 100644 --- a/netbox/resource_netbox_available_ip_address.go +++ b/netbox/resource_netbox_available_ip_address.go @@ -46,6 +46,11 @@ This resource will retrieve the next available IP address from a given prefix or Type: schema.TypeString, Computed: true, }, + "external_assignment": { + Type: schema.TypeBool, + Optional: true, + ConflictsWith: []string{"interface_id", "virtual_machine_interface_id", "device_interface_id"}, + }, "interface_id": { Type: schema.TypeInt, Optional: true, @@ -152,7 +157,10 @@ func resourceNetboxAvailableIPAddressRead(d *schema.ResourceData, m interface{}) } ipAddress := res.GetPayload() - if ipAddress.AssignedObjectID != nil { + + externallyAssigned := d.Get("external_assignment").(bool) + + if !externallyAssigned && ipAddress.AssignedObjectID != nil { vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") @@ -220,21 +228,41 @@ func resourceNetboxAvailableIPAddressUpdate(d *schema.ResourceData, m interface{ deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") - switch { - case vmInterfaceID != nil: - data.AssignedObjectType = strToPtr("virtualization.vminterface") - data.AssignedObjectID = vmInterfaceID - case deviceInterfaceID != nil: - data.AssignedObjectType = strToPtr("dcim.interface") - data.AssignedObjectID = deviceInterfaceID - // if interfaceID is given, object_type must be set as well - case interfaceID != nil: - data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) - data.AssignedObjectID = interfaceID - // default = ip is not linked to anything - default: - data.AssignedObjectType = strToPtr("") - data.AssignedObjectID = nil + // if assignment is done externally, we just pull the information, if any + if d.Get("external_assignment").(bool) { + params := ipam.NewIpamIPAddressesReadParams().WithID(id) + + res, err := api.Ipam.IpamIPAddressesRead(params, nil) + if err != nil { + return err + } + + ipAddress := res.GetPayload() + + if ipAddress.AssignedObjectType != nil { + data.AssignedObjectType = ipAddress.AssignedObjectType + data.AssignedObjectID = ipAddress.AssignedObjectID + } else { + data.AssignedObjectType = strToPtr("") + data.AssignedObjectID = nil + } + } else { + switch { + case vmInterfaceID != nil: + data.AssignedObjectType = strToPtr("virtualization.vminterface") + data.AssignedObjectID = vmInterfaceID + case deviceInterfaceID != nil: + data.AssignedObjectType = strToPtr("dcim.interface") + data.AssignedObjectID = deviceInterfaceID + // if interfaceID is given, object_type must be set as well + case interfaceID != nil: + data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) + data.AssignedObjectID = interfaceID + // default = ip is not linked to anything + default: + data.AssignedObjectType = strToPtr("") + data.AssignedObjectID = nil + } } data.Tags, _ = getNestedTagListFromResourceDataSet(api, d.Get(tagsKey)) diff --git a/netbox/resource_netbox_available_ip_address_test.go b/netbox/resource_netbox_available_ip_address_test.go index de5a9f7f..f0e861a2 100644 --- a/netbox/resource_netbox_available_ip_address_test.go +++ b/netbox/resource_netbox_available_ip_address_test.go @@ -285,6 +285,116 @@ resource "netbox_available_ip_address" "test" { }) } +func TestAccNetboxAvailableIPAddress_deviceByObjectType_external(t *testing.T) { + startAddress := "1.4.7.1/24" + endAddress := "1.4.7.50/24" + testSlug := "av_ipa_dev_ot_ext" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_range" "test_range" { + start_address = "%s" + end_address = "%s" +} +resource "netbox_available_ip_address" "test" { + ip_range_id = netbox_ip_range.test_range.id + status = "active" + external_assignment = true + dns_name = "test_range.mydomain.local" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_available_ip_address.test.id + object_type = "dcim.interface" + interface_id = netbox_device_interface.test.id +}`, startAddress, endAddress), + }, + // we update the description, to see if the ip and assignment don't conflict + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_range" "test_range" { + start_address = "%s" + end_address = "%s" +} +resource "netbox_available_ip_address" "test" { + ip_range_id = netbox_ip_range.test_range.id + status = "active" + external_assignment = true + dns_name = "test_range.mydomain.local" + description = "update" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_available_ip_address.test.id + object_type = "dcim.interface" + interface_id = netbox_device_interface.test.id +}`, startAddress, endAddress), + }, + { + ResourceName: "netbox_available_ip_address.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ip_range_id", "external_assignment", "interface_id", "object_type"}, + }, + }, + }) +} + +func TestAccNetboxAvailableIPAddress_deviceByFieldName_external(t *testing.T) { + startAddress := "1.3.7.1/24" + endAddress := "1.3.7.50/24" + testSlug := "av_ipa_dev_fn_ext" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_range" "test_range" { + start_address = "%s" + end_address = "%s" +} +resource "netbox_available_ip_address" "test" { + ip_range_id = netbox_ip_range.test_range.id + status = "active" + external_assignment = true + dns_name = "test_range.mydomain.local" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_available_ip_address.test.id + device_interface_id = netbox_device_interface.test.id +}`, startAddress, endAddress), + }, + // we update the description, to see if the ip and assignment don't conflict + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_range" "test_range" { + start_address = "%s" + end_address = "%s" +} +resource "netbox_available_ip_address" "test" { + ip_range_id = netbox_ip_range.test_range.id + status = "active" + external_assignment = true + dns_name = "test_range.mydomain.local" + description = "update" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_available_ip_address.test.id + device_interface_id = netbox_device_interface.test.id +}`, startAddress, endAddress), + }, + { + ResourceName: "netbox_available_ip_address.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ip_range_id", "external_assignment", "interface_id", "object_type"}, + }, + }, + }) +} + func init() { resource.AddTestSweepers("netbox_available_ip_address", &resource.Sweeper{ Name: "netbox_available_ip_address", diff --git a/netbox/resource_netbox_ip_address.go b/netbox/resource_netbox_ip_address.go index bd8d39db..73410a5c 100644 --- a/netbox/resource_netbox_ip_address.go +++ b/netbox/resource_netbox_ip_address.go @@ -33,6 +33,11 @@ func resourceNetboxIPAddress() *schema.Resource { Required: true, ValidateFunc: validation.IsCIDR, }, + "external_assignment": { + Type: schema.TypeBool, + Optional: true, + ConflictsWith: []string{"interface_id", "virtual_machine_interface_id", "device_interface_id"}, + }, "interface_id": { Type: schema.TypeInt, Optional: true, @@ -185,7 +190,10 @@ func resourceNetboxIPAddressRead(d *schema.ResourceData, m interface{}) error { } ipAddress := res.GetPayload() - if ipAddress.AssignedObjectID != nil { + + externallyAssigned := d.Get("external_assignment").(bool) + + if !externallyAssigned && ipAddress.AssignedObjectID != nil { vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") @@ -278,21 +286,41 @@ func resourceNetboxIPAddressUpdate(d *schema.ResourceData, m interface{}) error deviceInterfaceID := getOptionalInt(d, "device_interface_id") interfaceID := getOptionalInt(d, "interface_id") - switch { - case vmInterfaceID != nil: - data.AssignedObjectType = strToPtr("virtualization.vminterface") - data.AssignedObjectID = vmInterfaceID - case deviceInterfaceID != nil: - data.AssignedObjectType = strToPtr("dcim.interface") - data.AssignedObjectID = deviceInterfaceID - // if interfaceID is given, object_type must be set as well - case interfaceID != nil: - data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) - data.AssignedObjectID = interfaceID - // default = ip is not linked to anything - default: - data.AssignedObjectType = strToPtr("") - data.AssignedObjectID = nil + // if assignment is done externally, we just pull the information, if any + if d.Get("external_assignment").(bool) { + params := ipam.NewIpamIPAddressesReadParams().WithID(id) + + res, err := api.Ipam.IpamIPAddressesRead(params, nil) + if err != nil { + return err + } + + ipAddress := res.GetPayload() + + if ipAddress.AssignedObjectType != nil { + data.AssignedObjectType = ipAddress.AssignedObjectType + data.AssignedObjectID = ipAddress.AssignedObjectID + } else { + data.AssignedObjectType = strToPtr("") + data.AssignedObjectID = nil + } + } else { + switch { + case vmInterfaceID != nil: + data.AssignedObjectType = strToPtr("virtualization.vminterface") + data.AssignedObjectID = vmInterfaceID + case deviceInterfaceID != nil: + data.AssignedObjectType = strToPtr("dcim.interface") + data.AssignedObjectID = deviceInterfaceID + // if interfaceID is given, object_type must be set as well + case interfaceID != nil: + data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) + data.AssignedObjectID = interfaceID + // default = ip is not linked to anything + default: + data.AssignedObjectType = strToPtr("") + data.AssignedObjectID = nil + } } data.Tags, _ = getNestedTagListFromResourceDataSet(api, d.Get(tagsKey)) diff --git a/netbox/resource_netbox_ip_address_assignment.go b/netbox/resource_netbox_ip_address_assignment.go new file mode 100644 index 00000000..17c39f9a --- /dev/null +++ b/netbox/resource_netbox_ip_address_assignment.go @@ -0,0 +1,211 @@ +package netbox + +import ( + "strconv" + + "github.com/fbreckle/go-netbox/netbox/client" + "github.com/fbreckle/go-netbox/netbox/client/ipam" + "github.com/fbreckle/go-netbox/netbox/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +var resourceNetboxIPAddressAssignmentObjectTypeOptions = []string{"virtualization.vminterface", "dcim.interface"} + +func resourceNetboxIPAddressAssignment() *schema.Resource { + return &schema.Resource{ + Create: resourceNetboxIPAddressAssignmentCreate, + Read: resourceNetboxIPAddressAssignmentRead, + Update: resourceNetboxIPAddressAssignmentUpdate, + Delete: resourceNetboxIPAddressAssignmentDelete, + + Description: `:meta:subcategory:IP Address Management (IPAM):From the [official documentation](https://docs.netbox.dev/en/stable/features/ipam/#ip-addresses): + +> Assigns a NetBox Device, physical or virtual, to an already constructed IP address. +> +> In cases where the device assigned to the IP Address is not yet known when constructing the IP address (using either netbox_available_ip_address or netbox_ip_address), this resource allows assigning it afterwards. +> +> A typical scenario is when you statically allocate IP's to virtual machines and use netbox_available_ip_address to fetch that IP, but where the netbox_virtual_machine or netbox_interface can only be constructed after having started the virtual machine.`, + + Schema: map[string]*schema.Schema{ + "ip_address_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "interface_id": { + Type: schema.TypeInt, + Optional: true, + RequiredWith: []string{"object_type"}, + }, + "object_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(resourceNetboxIPAddressAssignmentObjectTypeOptions, false), + Description: buildValidValueDescription(resourceNetboxIPAddressAssignmentObjectTypeOptions), + RequiredWith: []string{"interface_id"}, + }, + "virtual_machine_interface_id": { + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"interface_id", "device_interface_id"}, + }, + "device_interface_id": { + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"interface_id", "virtual_machine_interface_id"}, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceNetboxIPAddressAssignmentCreate(d *schema.ResourceData, m interface{}) error { + id := d.Get("ip_address_id").(int) + + d.SetId(strconv.Itoa(id)) + + return resourceNetboxIPAddressAssignmentUpdate(d, m) +} + +func resourceNetboxIPAddressAssignmentRead(d *schema.ResourceData, m interface{}) error { + api := m.(*client.NetBoxAPI) + + id, _ := strconv.ParseInt(d.Id(), 10, 64) + params := ipam.NewIpamIPAddressesReadParams().WithID(id) + + res, err := api.Ipam.IpamIPAddressesRead(params, nil) + if err != nil { + if errresp, ok := err.(*ipam.IpamIPAddressesReadDefault); ok { + errorcode := errresp.Code() + if errorcode == 404 { + // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html + d.SetId("") + return nil + } + } + return err + } + + ipAddress := res.GetPayload() + if ipAddress.AssignedObjectID != nil { + vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") + deviceInterfaceID := getOptionalInt(d, "device_interface_id") + interfaceID := getOptionalInt(d, "interface_id") + + switch { + case vmInterfaceID != nil: + d.Set("virtual_machine_interface_id", ipAddress.AssignedObjectID) + case deviceInterfaceID != nil: + d.Set("device_interface_id", ipAddress.AssignedObjectID) + // if interfaceID is given, object_type must be set as well + case interfaceID != nil: + d.Set("object_type", ipAddress.AssignedObjectType) + d.Set("interface_id", ipAddress.AssignedObjectID) + } + } else { + d.Set("interface_id", nil) + d.Set("object_type", "") + } + + d.Set("ip_address_id", id) + + return nil +} + +func resourceNetboxIPAddressAssignmentUpdate(d *schema.ResourceData, m interface{}) error { + api := m.(*client.NetBoxAPI) + + id, _ := strconv.ParseInt(d.Id(), 10, 64) + + params := ipam.NewIpamIPAddressesReadParams().WithID(id) + + res, err := api.Ipam.IpamIPAddressesRead(params, nil) + if err != nil { + if errresp, ok := err.(*ipam.IpamIPAddressesReadDefault); ok { + errorcode := errresp.Code() + if errorcode == 404 { + // If the ID is updated to blank, this tells Terraform the resource no longer exists (maybe it was destroyed out of band). Just like the destroy callback, the Read function should gracefully handle this case. https://www.terraform.io/docs/extend/writing-custom-providers.html + d.SetId("") + return nil + } + } + return err + } + + ipAddress := res.GetPayload() + data := models.WritableIPAddress{} + + data.Address = ipAddress.Address + if ipAddress.Status != nil { + data.Status = *ipAddress.Status.Value + } + + data.Description = ipAddress.Description + if ipAddress.Role != nil { + data.Role = *ipAddress.Role.Value + } + data.DNSName = ipAddress.DNSName + if ipAddress.Vrf != nil { + data.Vrf = &ipAddress.Vrf.ID + } + if ipAddress.Tenant != nil { + data.Tenant = &ipAddress.Tenant.ID + } + if ipAddress.NatInside != nil { + data.NatInside = &ipAddress.NatInside.ID + } + + tags := make([]*models.NestedTag, len(ipAddress.Tags)) + for i, t := range ipAddress.Tags { + tags[i] = &models.NestedTag{Name: t.Name, Slug: t.Slug, Color: t.Color} + } + data.Tags = tags + + outsideNat := make([]*models.NestedIPAddress, len(ipAddress.NatOutside)) + for i, t := range ipAddress.NatOutside { + outsideNat[i] = &models.NestedIPAddress{Address: t.Address} + } + data.NatOutside = outsideNat + + vmInterfaceID := getOptionalInt(d, "virtual_machine_interface_id") + deviceInterfaceID := getOptionalInt(d, "device_interface_id") + interfaceID := getOptionalInt(d, "interface_id") + + switch { + case vmInterfaceID != nil: + data.AssignedObjectType = strToPtr("virtualization.vminterface") + data.AssignedObjectID = vmInterfaceID + case deviceInterfaceID != nil: + data.AssignedObjectType = strToPtr("dcim.interface") + data.AssignedObjectID = deviceInterfaceID + // if interfaceID is given, object_type must be set as well + case interfaceID != nil: + data.AssignedObjectType = strToPtr(d.Get("object_type").(string)) + data.AssignedObjectID = interfaceID + // default = ip is not linked to anything + default: + data.AssignedObjectType = strToPtr("") + data.AssignedObjectID = nil + } + + params2 := ipam.NewIpamIPAddressesPartialUpdateParams().WithID(id).WithData(&data) + + _, err2 := api.Ipam.IpamIPAddressesPartialUpdate(params2, nil) + if err2 != nil { + return err2 + } + + return nil +} + +func resourceNetboxIPAddressAssignmentDelete(d *schema.ResourceData, m interface{}) error { + d.Set("interface_id", nil) + d.Set("object_type", "") + d.Set("virtual_machine_interface_id", nil) + d.Set("device_interface_id", nil) + + return resourceNetboxIPAddressAssignmentUpdate(d, m) +} diff --git a/netbox/resource_netbox_ip_address_assignment_test.go b/netbox/resource_netbox_ip_address_assignment_test.go new file mode 100644 index 00000000..0044979c --- /dev/null +++ b/netbox/resource_netbox_ip_address_assignment_test.go @@ -0,0 +1,340 @@ +package netbox + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/fbreckle/go-netbox/netbox/client" + "github.com/fbreckle/go-netbox/netbox/client/ipam" + "github.com/fbreckle/go-netbox/netbox/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func testAccNetboxIPAddressAssignmentFullDependencies(testName string, testIP string, testIP2 string) string { + return fmt.Sprintf(` +resource "netbox_tag" "test" { + name = "%[1]s" +} + +resource "netbox_tenant" "test" { + name = "%[1]s" +} + +resource "netbox_vrf" "test" { + name = "%[1]s" +} + +resource "netbox_cluster_type" "test" { + name = "%[1]s" +} + +resource "netbox_cluster" "test" { + name = "%[1]s" + cluster_type_id = netbox_cluster_type.test.id +} + +resource "netbox_virtual_machine" "test" { + name = "%[1]s" + cluster_id = netbox_cluster.test.id +} + +resource "netbox_interface" "test" { + name = "%[1]s" + virtual_machine_id = netbox_virtual_machine.test.id +} + +resource "netbox_ip_address" "outer" { + ip_address = "%[3]s" + status = "active" + tags = [netbox_tag.test.name] +} + +resource "netbox_ip_address" "test" { + ip_address = "%[2]s" + status = "active" + tags = [netbox_tag.test.name] + dns_name = "abc.example.com" + description = "abc" + role = "anycast" + nat_inside_address_id = netbox_ip_address.outer.id +} +`, testName, testIP, testIP2) +} + +func testAccNetboxIPAddressAssignmentFullDeviceDependencies(testName string, testIP string) string { + return fmt.Sprintf(` +resource "netbox_tag" "test" { + name = "%[1]s" +} + +resource "netbox_site" "test" { + name = "%[1]s" + status = "active" +} + +resource "netbox_device_role" "test" { + name = "%[1]s" + color_hex = "123456" +} + +resource "netbox_manufacturer" "test" { + name = "%[1]s" +} + +resource "netbox_device_type" "test" { + model = "%[1]s" + manufacturer_id = netbox_manufacturer.test.id +} + +resource "netbox_device" "test" { + name = "%[1]s" + site_id = netbox_site.test.id + device_type_id = netbox_device_type.test.id + role_id = netbox_device_role.test.id +} +resource "netbox_device_interface" "test" { + name = "%[1]s" + device_id = netbox_device.test.id + type = "1000base-t" +} +resource "netbox_ip_address" "test" { + ip_address = "%[2]s" + status = "active" + tags = [netbox_tag.test.name] +} +`, testName, testIP) +} + +func TestAccNetboxIPAddressAssignment_basic(t *testing.T) { + testIP := "1.2.1.1/32" + testIP2 := "1.2.2.1/32" + testSlug := "ipaddress_assign" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressAssignmentFullDependencies(testName, testIP, testIP2) + fmt.Sprintf(` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "virtualization.vminterface" + interface_id = netbox_interface.test.id +} + +data "netbox_ip_addresses" "test" { + depends_on = [netbox_ip_address_assignment.test] + filter { + name = "ip_address" + value = "%[1]s" + } +} +`, testIP), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "ip_address_id", "netbox_ip_address.test", "id"), + resource.TestCheckResourceAttr("netbox_ip_address_assignment.test", "object_type", "virtualization.vminterface"), + resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.0.dns_name", "abc.example.com"), + resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.0.status", "active"), + resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.0.description", "abc"), + resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.0.role", "anycast"), + resource.TestCheckResourceAttr("data.netbox_ip_addresses.test", "ip_addresses.0.tags.0.name", testName), + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "interface_id", "netbox_interface.test", "id"), + ), + }, + { + ResourceName: "netbox_ip_address_assignment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, + }, + }, + }) +} + +func TestAccNetboxIPAddressAssignment_deviceByObjectType(t *testing.T) { + testIP := "1.2.1.2/32" + testSlug := "ipadr_dev_ot_assign" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressAssignmentFullDeviceDependencies(testName, testIP) + ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "dcim.interface" + interface_id = netbox_device_interface.test.id +}`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "ip_address_id", "netbox_ip_address.test", "id"), + resource.TestCheckResourceAttr("netbox_ip_address_assignment.test", "object_type", "dcim.interface"), + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "interface_id", "netbox_device_interface.test", "id"), + ), + }, + { + ResourceName: "netbox_ip_address_assignment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"interface_id", "object_type"}, + }, + }, + }) +} + +func TestAccNetboxIPAddressAssignment_vmSwitchStyle(t *testing.T) { + testIP := "1.2.1.9/32" + testIP2 := "1.2.2.9/32" + testSlug := "ipadr_vm_sw_assign" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressAssignmentFullDependencies(testName, testIP, testIP2) + ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "virtualization.vminterface" + interface_id = netbox_interface.test.id +}`, + }, + { + Config: testAccNetboxIPAddressAssignmentFullDependencies(testName, testIP, testIP2) + ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + virtual_machine_interface_id = netbox_interface.test.id +}`, + }, + { + ResourceName: "netbox_ip_address_assignment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"interface_id", "object_type", "virtual_machine_interface_id"}, + }, + }, + }) +} + +// TestAccNetboxIPAddressAssignment_deviceByFieldName tests if creating an ip address and linking it to a device via the `device_interface_id` field works +func TestAccNetboxIPAddressAssignment_deviceByFieldName(t *testing.T) { + testIP := "1.2.1.4/32" + testSlug := "ipadr_dev_fn_assign" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressAssignmentFullDeviceDependencies(testName, testIP) + ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + device_interface_id = netbox_device_interface.test.id +}`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "ip_address_id", "netbox_ip_address.test", "id"), + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "device_interface_id", "netbox_device_interface.test", "id"), + ), + }, + { + ResourceName: "netbox_ip_address_assignment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"device_interface_id"}, + }, + }, + }) +} + +func TestAccNetboxIPAddressAssignment_vmByFieldName(t *testing.T) { + testIP := "1.2.1.5/32" + testIP2 := "1.2.2.5/32" + testSlug := "ipadr_vm_fn_assign" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressAssignmentFullDependencies(testName, testIP, testIP2) + ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + virtual_machine_interface_id = netbox_interface.test.id +}`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "ip_address_id", "netbox_ip_address.test", "id"), + resource.TestCheckResourceAttrPair("netbox_ip_address_assignment.test", "virtual_machine_interface_id", "netbox_interface.test", "id"), + ), + }, + { + ResourceName: "netbox_ip_address_assignment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"virtual_machine_interface_id"}, + }, + }, + }) +} + +func TestAccNetboxIPAddressAssignment_invalidConfig(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ // api.Ipam.IpamIPAddressesPartialUpdate() + // NewPatchedWritableIPAddressRequest() + + { + Config: ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = 1 + object_type = "dcim.interface" +}`, + ExpectError: regexp.MustCompile(".*all of `interface_id,object_type` must be specified.*"), + }, + { + Config: ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = 1 + interface_id = 1 +}`, + ExpectError: regexp.MustCompile(".*all of `interface_id,object_type` must be specified.*"), + }, + { + Config: ` +resource "netbox_ip_address_assignment" "test" { + ip_address_id = 1 + virtual_machine_interface_id = 1 + interface_id = 1 + object_type = "dcim.interface" +}`, + ExpectError: regexp.MustCompile(".*conflicts with interface_id.*"), + }, + }, + }) +} + +func init() { + resource.AddTestSweepers("netbox_ip_address_assignment", &resource.Sweeper{ + Name: "netbox_ip_address_assignment", + Dependencies: []string{}, + F: func(region string) error { + m, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("Error getting client: %s", err) + } + api := m.(*client.NetBoxAPI) + params := ipam.NewIpamIPAddressesListParams() + res, err := api.Ipam.IpamIPAddressesList(params, nil) + if err != nil { + return err + } + for _, ipAddress := range res.GetPayload().Results { + if len(ipAddress.Tags) > 0 && (ipAddress.Tags[0] == &models.NestedTag{Name: strToPtr("acctest"), Slug: strToPtr("acctest")}) { + deleteParams := ipam.NewIpamIPAddressesDeleteParams().WithID(ipAddress.ID) + _, err := api.Ipam.IpamIPAddressesDelete(deleteParams, nil) + if err != nil { + return err + } + log.Print("[DEBUG] Deleted an ip address") + } + } + return nil + }, + }) +} diff --git a/netbox/resource_netbox_ip_address_test.go b/netbox/resource_netbox_ip_address_test.go index e6c55721..655d0180 100644 --- a/netbox/resource_netbox_ip_address_test.go +++ b/netbox/resource_netbox_ip_address_test.go @@ -484,6 +484,181 @@ resource "netbox_ip_address" "test" { }) } +func TestAccNetboxIPAddress_deviceByObjectType_external(t *testing.T) { + testIP := "1.1.1.12/32" + testSlug := "ipadr_dev_ot_ext" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "dcim.interface" + interface_id = netbox_device_interface.test.id +}`, testIP), + }, + // we update the description, to see if the ip and assignment don't conflict + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" + description = "update" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "dcim.interface" + interface_id = netbox_device_interface.test.id +}`, testIP), + }, + { + ResourceName: "netbox_ip_address.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"external_assignment", "interface_id", "object_type"}, + }, + }, + }) +} + +func TestAccNetboxIPAddress_vmByObjectType_external(t *testing.T) { + testIP := "1.1.1.13/32" + testSlug := "ipadr_vm_ot_ext" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "virtualization.vminterface" + interface_id = netbox_interface.test.id +}`, testIP), + }, + // we update the description, to see if the ip and assignment don't conflict + { + Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" + description = "update" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + object_type = "virtualization.vminterface" + interface_id = netbox_interface.test.id +}`, testIP), + }, + { + ResourceName: "netbox_ip_address.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"external_assignment", "interface_id", "object_type"}, + }, + }, + }) +} + +// TestAccNetboxIPAddress_deviceByFieldName tests if creating an ip address and linking it to a device via the `device_interface_id` field works +func TestAccNetboxIPAddress_deviceByFieldName_external(t *testing.T) { + testIP := "1.1.1.14/32" + testSlug := "ipadr_dev_fn_ext" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + device_interface_id = netbox_device_interface.test.id +}`, testIP), + }, + { + Config: testAccNetboxIPAddressFullDeviceDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" + description = "update" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + device_interface_id = netbox_device_interface.test.id +}`, testIP), + }, + { + ResourceName: "netbox_ip_address.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"external_assignment", "interface_id", "object_type"}, + }, + }, + }) +} + +func TestAccNetboxIPAddress_vmByFieldName_external(t *testing.T) { + testIP := "1.1.1.15/32" + testSlug := "ipadr_vm_fn_ext" + testName := testAccGetTestName(testSlug) + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + virtual_machine_interface_id = netbox_interface.test.id +}`, testIP), + }, + { + Config: testAccNetboxIPAddressFullDependencies(testName) + fmt.Sprintf(` +resource "netbox_ip_address" "test" { + ip_address = "%s" + external_assignment = true + status = "active" + description = "update" +} +resource "netbox_ip_address_assignment" "test" { + ip_address_id = netbox_ip_address.test.id + virtual_machine_interface_id = netbox_interface.test.id +}`, testIP), + }, + { + ResourceName: "netbox_ip_address.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"external_assignment", "interface_id", "object_type"}, + }, + }, + }) +} + func init() { resource.AddTestSweepers("netbox_ip_address", &resource.Sweeper{ Name: "netbox_ip_address",