diff --git a/docs/resources/available_ip_address.md b/docs/resources/available_ip_address.md index e3705cc9..543e0431 100644 --- a/docs/resources/available_ip_address.md +++ b/docs/resources/available_ip_address.md @@ -28,7 +28,7 @@ Per [the docs](https://netbox.readthedocs.io/en/stable/models/ipam/ipaddress/): > * DHCP > * SLAAC (IPv6 Stateless Address Autoconfiguration) -This resource will retrieve the next available IP address from a given prefix or IP range (specified by ID) +This resource will retrieve the next available IP address from a given prefix, IP range, prefixes or IP ranges (specified by ID) ## Example Usage ### Creating an IP in a prefix @@ -77,6 +77,34 @@ resource "netbox_available_ip_address" "myvm-ip" { } ``` +### Creating Ip addreses from multiple subnets and ranges + +```terraform +data "netbox_prefix" "test" { + count = 3 + cidr = "10.&{count.index}.0.0/30" + is_pool = true +} + +data "netbox_ip_range" "test" { + count = 3 + start_address = "2.0.${count.index}.0" + end_address = "2.0.${count.index}.3" +} + +resource "netbox_available_ip_address" "from_prefix" { + count = 3 * 4 + prefix_id = data.netbox_prefix.test[count.index].id + status = "active" +} + +resource "netbox_available_ip_address" "from_range" { + count = 3 * 4 + ip_range_id = data.netbox_ip_range.test[count.index].id + status = "active" +} +``` + ## Schema @@ -86,9 +114,11 @@ resource "netbox_available_ip_address" "myvm-ip" { - `device_interface_id` (Number) Conflicts with `interface_id` and `virtual_machine_interface_id`. - `dns_name` (String) - `interface_id` (Number) Required when `object_type` is set. -- `ip_range_id` (Number) Exactly one of `prefix_id` or `ip_range_id` must be given. +- `ip_range_id` (Number) Exactly one of `prefix_id`, `ip_range_id`, `prefix_ids` or `ip_range_ids` must be given. +- `ip_range_ids` (List(number)) Exactly one of `prefix_id`, `ip_range_id`, `prefix_ids` or `ip_range_ids` must be given. - `object_type` (String) Valid values are `virtualization.vminterface` and `dcim.interface`. Required when `interface_id` is set. -- `prefix_id` (Number) Exactly one of `prefix_id` or `ip_range_id` must be given. +- `prefix_id` (Number) Exactly one of `prefix_id`, `ip_range_id`, `prefix_ids` or `ip_range_ids` must be given. +- `prefix_ids` (List(number)) Exactly one of `prefix_id`, `ip_range_id`, `prefix_ids` or `ip_range_ids` must be given. - `role` (String) Valid values are `loopback`, `secondary`, `anycast`, `vip`, `vrrp`, `hsrp`, `glbp` and `carp`. - `status` (String) Valid values are `active`, `reserved`, `deprecated`, `dhcp` and `slaac`. Defaults to `active`. - `tags` (Set of String) @@ -100,5 +130,4 @@ resource "netbox_available_ip_address" "myvm-ip" { - `id` (String) The ID of this resource. - `ip_address` (String) - - +- `selected_id` (Number) The ID of the selected range or prefix. diff --git a/netbox/resource_netbox_available_ip_address.go b/netbox/resource_netbox_available_ip_address.go index 5e2a1869..f7357d8b 100644 --- a/netbox/resource_netbox_available_ip_address.go +++ b/netbox/resource_netbox_available_ip_address.go @@ -1,6 +1,7 @@ package netbox import ( + "fmt" "strconv" "github.com/fbreckle/go-netbox/netbox/client" @@ -35,12 +36,33 @@ This resource will retrieve the next available IP address from a given prefix or "prefix_id": { Type: schema.TypeInt, Optional: true, - ExactlyOneOf: []string{"prefix_id", "ip_range_id"}, + ExactlyOneOf: []string{"prefix_id", "ip_range_id", "prefix_ids", "ip_range_ids"}, + }, + "prefix_ids": { + Type: schema.TypeList, + Optional: true, + ExactlyOneOf: []string{"prefix_id", "ip_range_id", "prefix_ids", "ip_range_ids"}, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, }, "ip_range_id": { Type: schema.TypeInt, Optional: true, - ExactlyOneOf: []string{"prefix_id", "ip_range_id"}, + ExactlyOneOf: []string{"prefix_id", "ip_range_id", "prefix_ids", "ip_range_ids"}, + }, + "ip_range_ids": { + Type: schema.TypeList, + Optional: true, + ExactlyOneOf: []string{"prefix_id", "ip_range_id", "prefix_ids", "ip_range_ids"}, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + "selected_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the prefix or IP range that was used to generate the IP address.", }, "ip_address": { Type: schema.TypeString, @@ -105,34 +127,110 @@ This resource will retrieve the next available IP address from a given prefix or } } +type ipamIPAvailableIpsCreateCreated interface { + GetPayload() []*models.IPAddress +} + +func payloadHandlerCreate(res ipamIPAvailableIpsCreateCreated) (int64, string, error) { + if res == nil { + // Ranges causes issues here. + return 0, "", fmt.Errorf("payload is nil, this could be caused by providing the wrong ID") + } + if len(res.GetPayload()) != 1 { + return 0, "", fmt.Errorf("expected 1 ip address, got %d", len(res.GetPayload())) + } + return res.GetPayload()[0].ID, *res.GetPayload()[0].Address, nil +} + func resourceNetboxAvailableIPAddressCreate(d *schema.ResourceData, m interface{}) error { api := m.(*client.NetBoxAPI) - prefixID := int64(d.Get("prefix_id").(int)) - vrfID := int64(int64(d.Get("vrf_id").(int))) - rangeID := int64(d.Get("ip_range_id").(int)) + var selectedID int64 + var err error + + vrfID := int64(d.Get("vrf_id").(int)) nestedvrf := models.NestedVRF{ ID: vrfID, } data := models.AvailableIP{ Vrf: &nestedvrf, } - if prefixID != 0 { + + var res ipamIPAvailableIpsCreateCreated + if prefixID := int64(d.Get("prefix_id").(int)); prefixID != 0 { params := ipam.NewIpamPrefixesAvailableIpsCreateParams().WithID(prefixID).WithData([]*models.AvailableIP{&data}) - res, _ := api.Ipam.IpamPrefixesAvailableIpsCreate(params, nil) - // Since we generated the ip_address, set that now - d.SetId(strconv.FormatInt(res.Payload[0].ID, 10)) - d.Set("ip_address", *res.Payload[0].Address) + res, err = api.Ipam.IpamPrefixesAvailableIpsCreate(params, nil) + if err != nil { + return fmt.Errorf("unable to create a ip address for prefix: %v, err: %w", prefixID, err) + } + selectedID = prefixID } - if rangeID != 0 { + if rangeID := int64(d.Get("ip_range_id").(int)); rangeID != 0 { + selectedID = rangeID params := ipam.NewIpamIPRangesAvailableIpsCreateParams().WithID(rangeID).WithData([]*models.AvailableIP{&data}) - res, _ := api.Ipam.IpamIPRangesAvailableIpsCreate(params, nil) - // Since we generated the ip_address, set that now - d.SetId(strconv.FormatInt(res.Payload[0].ID, 10)) - d.Set("ip_address", *res.Payload[0].Address) + res, err = api.Ipam.IpamIPRangesAvailableIpsCreate(params, nil) + if err != nil { + return fmt.Errorf("unable to create a ip address for ip Range: %v, err: %w", rangeID, err) + } } + if prefixIDs, err := assertInterfaceToInt64Slice(d.Get("prefix_ids")); err == nil && len(prefixIDs) > 0 { + for _, selectedID := range prefixIDs { + // q: Ask for forgivnes or check first? + params := ipam.NewIpamPrefixesAvailableIpsCreateParams().WithID(selectedID).WithData([]*models.AvailableIP{&data}) + res, err = api.Ipam.IpamPrefixesAvailableIpsCreate(params, nil) + if err == nil { + // There is avalible ips + break + } + } + if err != nil { + return fmt.Errorf("unable to create a ip address for prefixes: %v, err: %w", prefixIDs, err) + } + } else if err != nil { + return fmt.Errorf("unable to convert prefixIDs to []int64: %w", err) + } + if rangeIDs, err := assertInterfaceToInt64Slice(d.Get("ip_range_ids")); err == nil && len(rangeIDs) > 0 { + // Try Ranges until one does not return an error + for _, selectedID = range rangeIDs { + params := ipam.NewIpamIPRangesAvailableIpsCreateParams().WithID(selectedID).WithData([]*models.AvailableIP{&data}) + res, err = api.Ipam.IpamIPRangesAvailableIpsCreate(params, nil) + if err == nil { + // There is avalible ips + break + } + } + if err != nil { + return fmt.Errorf("unable to create a ip address for ip Ranges: %v, err: %w", rangeIDs, err) + } + } else if err != nil { + return fmt.Errorf("unable to convert rangeIDs to []int64: %w", err) + } + netboxID, ipaddress, err := payloadHandlerCreate(res) + if err != nil { + return fmt.Errorf("unable to handle payload: %w", err) + } + d.SetId(strconv.FormatInt(netboxID, 10)) + d.Set("ip_address", ipaddress) + d.Set("selected_id", selectedID) return resourceNetboxAvailableIPAddressUpdate(d, m) } +func assertInterfaceToInt64Slice(x interface{}) ([]int64, error) { + var intSlice []int64 + var number int + var ok bool + xSlice, ok := x.([]interface{}) + if !ok { + return nil, fmt.Errorf("assertInterfaceToInt64Slice: Unable to convert x:%v to []interface{}", x) + } + for _, v := range xSlice { + if number, ok = v.(int); !ok { + return nil, fmt.Errorf("assertSliceInterfaceToInt64: Unable to convert number:%v to int", v) + } + intSlice = append(intSlice, int64(number)) + } + return intSlice, nil +} + func resourceNetboxAvailableIPAddressRead(d *schema.ResourceData, m interface{}) error { api := m.(*client.NetBoxAPI) id, _ := strconv.ParseInt(d.Id(), 10, 64) diff --git a/netbox/resource_netbox_available_ip_address_test.go b/netbox/resource_netbox_available_ip_address_test.go index de5a9f7f..63ab4e4f 100644 --- a/netbox/resource_netbox_available_ip_address_test.go +++ b/netbox/resource_netbox_available_ip_address_test.go @@ -41,6 +41,7 @@ resource "netbox_available_ip_address" "test" { }, }) } + func TestAccNetboxAvailableIPAddress_basic_range(t *testing.T) { startAddress := "1.1.5.1/24" endAddress := "1.1.5.50/24" @@ -285,6 +286,168 @@ resource "netbox_available_ip_address" "test" { }) } +func TestAccNetboxAvailableIPAddress_multiple_cidrs_prefixses(t *testing.T) { + testPrefix1 := "1.3.2.0/32" + testPrefix2 := "2.3.2.0/32" + testIP := "2.3.2.0/32" + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +resource "netbox_prefix" "test1" { + prefix = "%[1]s" + status = "active" + is_pool = true +} +resource "netbox_prefix" "test2" { + prefix = "%[2]s" + status = "active" + is_pool = true + } +resource "netbox_available_ip_address" "test" { + prefix_ids = [netbox_prefix.test1.id, netbox_prefix.test2.id] + status = "active" + dns_name = "test.mydomain.local" + role = "loopback" +} +resource "netbox_available_ip_address" "test2" { + depends_on = [netbox_available_ip_address.test] + prefix_ids = [netbox_prefix.test1.id, netbox_prefix.test2.id] + status = "active" + dns_name = "test.mydomain.local" + role = "loopback" +}`, testPrefix1, testPrefix2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "ip_address", testIP), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "status", "active"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "dns_name", "test.mydomain.local"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "role", "loopback"), + resource.TestCheckResourceAttrSet("netbox_available_ip_address.test", "selected_id"), + ), + }, + }, + }) +} + +func TestAccNetboxAvailableIPAddress_multiple_cidrs_ranges(t *testing.T) { + testIP := "1.4.2.0/28" + testIP2 := "1.4.2.1/28" + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ` +resource "netbox_ip_range" "test1" { + start_address = "1.4.2.0/28" + end_address = "1.4.2.2/28" +} +resource "netbox_ip_range" "test2" { + start_address = "2.1.2.0/28" + end_address = "2.1.2.2/28" +} +resource "netbox_available_ip_address" "test" { + ip_range_ids = [netbox_ip_range.test1.id, netbox_ip_range.test2.id] + status = "active" + dns_name = "test.mydomain.local" + role = "loopback" +} +resource "netbox_available_ip_address" "test2" { + depends_on = [netbox_available_ip_address.test] + ip_range_ids = [netbox_ip_range.test1.id, netbox_ip_range.test2.id] + status = "active" + dns_name = "test.mydomain.local" + role = "loopback" +}`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "ip_address", testIP), + resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "ip_address", testIP2), + resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "status", "active"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "dns_name", "test.mydomain.local"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test2", "role", "loopback"), + resource.TestCheckResourceAttrSet("netbox_available_ip_address.test", "selected_id"), + ), + }, + }, + }) +} + +func TestAccNetboxAvailableIPAddress_multiple_cidrs_overflow_prefix(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ` +resource "netbox_prefix" "test" { + count = 5 + prefix = "13.0.0.${4 * count.index}/30" + status = "active" + is_pool = true +} +// Consume most of the available IPs +resource "netbox_available_ip_address" "_test" { + count = 19 + prefix_ids = netbox_prefix.test.*.id + status = "active" + dns_name = "_test.mydomain.local" + role = "loopback" +} +resource "netbox_available_ip_address" "test" { + depends_on = [netbox_available_ip_address._test] + prefix_ids = netbox_prefix.test.*.id + status = "active" + dns_name = "test.mydomain.local" + role = "loopback" +}`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "ip_address", "13.0.0.19/30"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "status", "active"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "role", "loopback"), + resource.TestCheckResourceAttrSet("netbox_available_ip_address.test", "selected_id"), + ), + }, + }, + }) +} + +func TestAccNetboxAvailableIPAddress_multiple_cidrs_overflow_ranges(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ` +resource "netbox_ip_range" "test" { + count = 5 + start_address = "4.0.0.${4 * count.index}/32" + end_address = "4.0.0.${4 * count.index + 3}/32" +} +// Consume most of the available IPs +resource "netbox_available_ip_address" "_test" { + count = 19 + ip_range_ids = netbox_ip_range.test.*.id + status = "active" + dns_name = "test${count.index}.mydomain.local" + role = "loopback" +} +resource "netbox_available_ip_address" "test" { + depends_on = [netbox_available_ip_address._test] + ip_range_ids = netbox_ip_range.test.*.id + status = "active" + dns_name = "test.mydomain.local" + role = "loopback" +}`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "ip_address", "4.0.0.19/32"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "status", "active"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "dns_name", "test.mydomain.local"), + resource.TestCheckResourceAttr("netbox_available_ip_address.test", "role", "loopback"), + resource.TestCheckResourceAttrSet("netbox_available_ip_address.test", "selected_id"), + ), + }, + }, + }) +} + func init() { resource.AddTestSweepers("netbox_available_ip_address", &resource.Sweeper{ Name: "netbox_available_ip_address", diff --git a/netbox/resource_netbox_ip_range.go b/netbox/resource_netbox_ip_range.go index 5cce094f..75335dad 100644 --- a/netbox/resource_netbox_ip_range.go +++ b/netbox/resource_netbox_ip_range.go @@ -25,12 +25,14 @@ func resourceNetboxIPRange() *schema.Resource { Schema: map[string]*schema.Schema{ "start_address": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsCIDR, }, "end_address": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsCIDR, }, "status": { Type: schema.TypeString, diff --git a/netbox/validation.go b/netbox/validation.go index 83008b88..d92bfc6b 100644 --- a/netbox/validation.go +++ b/netbox/validation.go @@ -1,6 +1,8 @@ package netbox -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) const ( maxUint16 = ^uint16(0)