From 11083d01311dc938a144e42f75290dbc57527e9c Mon Sep 17 00:00:00 2001 From: pavel-z1 Date: Tue, 10 Mar 2020 14:01:12 +0200 Subject: [PATCH] Added Dynamic IP creation by using API method /addresses/first_free/{subnetId}/ --- README.md | 38 ++++ go.sum | 1 + plugin/providers/phpipam/provider.go | 9 +- .../resource_phpipam_first_free_address.go | 180 ++++++++++++++++++ 4 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 plugin/providers/phpipam/resource_phpipam_first_free_address.go diff --git a/README.md b/README.md index 193ef2db..8e97453c 100644 --- a/README.md +++ b/README.md @@ -798,6 +798,44 @@ The following attributes are exported: * `last_seen` - The last time this IP address answered ping probes. * `edit_date` - The last time this resource was modified. +#### The `phpipam_first_free_address` Resource - Dynamic IPs creation + +The `phpipam_first_free_address` resource allow to create automatically +new IP in defined network without execution Terraform data instruction. You can use it +to create several IP addresses automatically. This resource support the same arguments as +phpipam_address. An example usage is below. + +⚠️ **NOTE:** This is experimental new feature. You can use Terraform count +instruction. But be carefull, phpIPAM currently has a bug https://github.com/phpipam/phpipam/issues/2960 +Use resource with count option only with limitted terraform threads count: `terraform apply -parallelism=1` +. + +**Example:** + +``` +// Look up the subnet +data "phpipam_subnet" "subnet" { + subnet_address = "10.10.2.0" + subnet_mask = 24 +} + +// Reserve the address. Note that we use ignore_changes here to ensure that we +// don't end up re-allocating this address on future Terraform runs. +resource "phpipam_first_free_address" { + count = 3 + + subnet_id = data.phpipam_subnet.subnet.subnet_id + hostname = "tf-test-host.example.internal" + description = "Managed by Terraform" + + lifecycle { + ignore_changes = [ + subnet_id, + ] + } +} +``` + #### The `phpipam_section` Resource The `phpipam_section` resource manages a PHPIPAM section - a top-level category diff --git a/go.sum b/go.sum index 8295b96a..cea71f89 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,7 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.0.0-20160517064435-50d4dbd4eb0e/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= diff --git a/plugin/providers/phpipam/provider.go b/plugin/providers/phpipam/provider.go index 164f9a21..6ae0bb3b 100644 --- a/plugin/providers/phpipam/provider.go +++ b/plugin/providers/phpipam/provider.go @@ -36,10 +36,11 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "phpipam_address": resourcePHPIPAMAddress(), - "phpipam_section": resourcePHPIPAMSection(), - "phpipam_subnet": resourcePHPIPAMSubnet(), - "phpipam_vlan": resourcePHPIPAMVLAN(), + "phpipam_address": resourcePHPIPAMAddress(), + "phpipam_section": resourcePHPIPAMSection(), + "phpipam_subnet": resourcePHPIPAMSubnet(), + "phpipam_vlan": resourcePHPIPAMVLAN(), + "phpipam_first_free_address": resourcePHPIPAMFirstFreeAddress(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/plugin/providers/phpipam/resource_phpipam_first_free_address.go b/plugin/providers/phpipam/resource_phpipam_first_free_address.go new file mode 100644 index 00000000..76b0ea53 --- /dev/null +++ b/plugin/providers/phpipam/resource_phpipam_first_free_address.go @@ -0,0 +1,180 @@ +package phpipam + +import ( + "errors" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +// resourcePHPIPAMAddress returns the resource structure for the phpipam_address +// resource. +// +// Note that we use the data source read function here to pull down data, as +// read workflow is identical for both the resource and the data source. +func resourcePHPIPAMFirstFreeAddress() *schema.Resource { + return &schema.Resource{ + Create: resourcePHPIPAMFirstFreeAddressCreate, + Read: dataSourcePHPIPAMAddressRead, + Update: resourcePHPIPAMFirstFreeAddressUpdate, + Delete: resourcePHPIPAMFirstFreeAddressDelete, + Schema: resourceFirstFreeAddressSchema(), + } +} + +// bareAddressSchema returns a map[string]*schema.Schema with the schema used +// to represent a PHPIPAM address resource. This output should then be modified +// so that required and computed fields are set properly for both the data +// source and the resource. +func bareFirstFreeAddressSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "address_id": &schema.Schema{ + Type: schema.TypeInt, + }, + "subnet_id": &schema.Schema{ + Type: schema.TypeInt, + }, + "ip_address": &schema.Schema{ + Type: schema.TypeString, + }, + "is_gateway": &schema.Schema{ + Type: schema.TypeBool, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + }, + "hostname": &schema.Schema{ + Type: schema.TypeString, + }, + "mac_address": &schema.Schema{ + Type: schema.TypeString, + }, + "owner": &schema.Schema{ + Type: schema.TypeString, + }, + "state_tag_id": &schema.Schema{ + Type: schema.TypeInt, + }, + "skip_ptr_record": &schema.Schema{ + Type: schema.TypeBool, + }, + "ptr_record_id": &schema.Schema{ + Type: schema.TypeInt, + }, + "device_id": &schema.Schema{ + Type: schema.TypeInt, + }, + "switch_port_label": &schema.Schema{ + Type: schema.TypeString, + }, + "note": &schema.Schema{ + Type: schema.TypeString, + }, + "last_seen": &schema.Schema{ + Type: schema.TypeString, + }, + "exclude_ping": &schema.Schema{ + Type: schema.TypeBool, + }, + "edit_date": &schema.Schema{ + Type: schema.TypeString, + }, + "custom_fields": &schema.Schema{ + Type: schema.TypeMap, + }, + } +} + +// resourceAddressSchema returns the schema for the phpipam_address resource. +// It sets the required and optional fields, the latter defined in +// resourceAddressRequiredFields, and ensures that all optional and +// non-configurable fields are computed as well. +func resourceFirstFreeAddressSchema() map[string]*schema.Schema { + s := bareAddressSchema() + for k, v := range s { + switch { + // IP Address and Subnet ID are ForceNew + case k == "subnet_id": + v.Required = true + v.ForceNew = true + case k == "custom_fields": + v.Optional = true + case resourceAddressOptionalFields.Has(k): + v.Optional = true + v.Computed = true + default: + v.Computed = true + } + } + return s +} + +func resourcePHPIPAMFirstFreeAddressCreate(d *schema.ResourceData, meta interface{}) error { + // Get first free IP from provided subnet_id + subnet_id := d.Get("subnet_id").(int) + d.Set("subnet_id", nil) + + // Get address controller and start address creation + c := meta.(*ProviderPHPIPAMClient).addressesController + + in := expandAddress(d) + + out, err := c.CreateFirstFreeAddress(subnet_id, in) + if err != nil { + return err + } + d.Set("ip_address", out) + + // If we have custom fields, set them now. We need to get the IP address's ID + // beforehand. + if customFields, ok := d.GetOk("custom_fields"); ok { + addrs, err := c.GetAddressesByIP(out) + if err != nil { + return fmt.Errorf("Could not read IP address after creating: %s", err) + } + //addrs := d.Get("ip_address") + + if len(addrs) != 1 { + return errors.New("IP address either missing or multiple results returned by reading IP after creation") + } + + d.SetId(strconv.Itoa(addrs[0].ID)) + + if _, err := c.UpdateAddressCustomFields(addrs[0].ID, customFields.(map[string]interface{})); err != nil { + return err + } + } + + return dataSourcePHPIPAMAddressRead(d, meta) +} + +func resourcePHPIPAMFirstFreeAddressUpdate(d *schema.ResourceData, meta interface{}) error { + c := meta.(*ProviderPHPIPAMClient).addressesController + in := expandAddress(d) + + // IPAddress and SubnetID need to be removed for update requests. + in.IPAddress = "" + in.SubnetID = 0 + if _, err := c.UpdateAddress(in); err != nil { + return err + } + + if err := updateCustomFields(d, c); err != nil { + return err + } + + return dataSourcePHPIPAMAddressRead(d, meta) +} + +func resourcePHPIPAMFirstFreeAddressDelete(d *schema.ResourceData, meta interface{}) error { + c := meta.(*ProviderPHPIPAMClient).addressesController + in := expandAddress(d) + +// if _, err := c.DeleteAddress(in.ID, phpipam.BoolIntString(d.Get("remove_dns_on_delete").(bool))); err != nil { + if _, err := c.DeleteAddress(in.ID, false); err != nil { + return err + } + d.SetId("") + return nil +}