diff --git a/README.md b/README.md index 9e32757..edd59cc 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,35 @@ The following attributes are exported: listing. * `dns_resolver_id` - The ID of the DNS resolver to use in the section. +#### The `phpipam_first_free_subnet` Data Source +The `phpipam_first_free_subnet` data source returns first available subnet within given `master_subnet_id` for specified `subnet_mask` + +**Example:** +``` +data "phpipam_first_free_subnet" "subnet" { + master_subnet_id = "9" + subnet_mask = "26" +} + +output subnet_cidr_block { + value = "${data.phpipam_first_free_subnet.subnet.subnet_address}" +} +``` + +##### Argument Reference + +The data source takes the following parameters: +* `master_subnet_id` (Required) - The ID of the parent subnet for this subnet +* `subnet_mask` (Required) - The subnet mask of first available subnet, in bits. + +##### Attribute Reference + +The following attributes are exported: + + * `subnet_address` - The network address of the subnet + subnet. + + #### The `phpipam_subnet` Data Source The `phpipam_subnet` data source gets information on a subnet such as its ID @@ -425,6 +454,7 @@ resource "phpipam_address" { } ``` + **Example with `description_match`:** ``` @@ -823,6 +853,44 @@ The following attributes are exported: * `section_id` - The ID of the section in the PHPIPAM database. * `edit_date` - The date this resource was last edited. +#### The `phpipam_first_free_subnet` Resource +The `phpipam_first_free_subnet` resouce can be used to checkout and manage a subnet in PHPIPAM. It is same as creating subnet using `phpipam_subnet` resource, with an option to checkout first free subnet from given `master_subnet_id` and `subnet_mask` + +Example: +``` +resource "phpipam_first_free_subnet" "subnet" { + master_subnet_id = 9 + subnet_mask = 24 + custom_fields = { + CustomTestSubnets = "terraform-test" + } + +} + +``` +##### Argument Reference + +The resouce takes following parameters: +* `master_subnet_id` (Required) - The ID of the parent subnet for this subnet +* `subnet_mask` (Required) - The subnet mask, in bits. + +⚠️ **NOTE on custom fields:** PHPIPAM installations with custom fields must have +all fields set to optional when using this plugin. For more info see +[here](https://github.com/phpipam/phpipam/issues/1073). Further to this, either +ensure that your fields also do not have default values, or ensure the default +is set in your TF configuration. Diff loops may happen otherwise! + +##### Attribute Reference + +The following attributes are exported: + + * `subnet_id` - The ID of the subnet in the PHPIPAM database. + * `subnet_address` - The network address of the subnet + * `permissions` - A JSON representation of the permissions associated with this + subnet. + * `edit_date` - The date this resource was last updated. + + #### The `phpipam_subnet` Resource The `phpipam_subnet` resource can be used to create and manage a subnet in diff --git a/plugin/providers/phpipam/data_source_phpipam_first_free_subnet.go b/plugin/providers/phpipam/data_source_phpipam_first_free_subnet.go new file mode 100755 index 0000000..fc6bab8 --- /dev/null +++ b/plugin/providers/phpipam/data_source_phpipam_first_free_subnet.go @@ -0,0 +1,43 @@ +package phpipam + +import ( + "fmt" + "errors" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourcePHPIPAMFirstFreeSubnet() *schema.Resource { + return &schema.Resource{ + Read: dataSourcePHPIPAMFirstFreeSubnetRead, + Schema: map[string]*schema.Schema{ + "master_subnet_id": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "subnet_mask": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "subnet_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourcePHPIPAMFirstFreeSubnetRead(d *schema.ResourceData, meta interface{}) error { + c := meta.(*ProviderPHPIPAMClient).subnetsController + out, err := c.GetFirstFreeSubnet(d.Get("master_subnet_id").(int),d.Get("subnet_mask").(int)) + if err != nil { + return err + } + if out == "" { + return errors.New(fmt.Sprintf("Master Subnet has no free subnet of size %s", d.Get("subnet_mask"))) + } + + d.SetId(out) + d.Set("subnet_address", out) + + return nil +} diff --git a/plugin/providers/phpipam/provider.go b/plugin/providers/phpipam/provider.go index e36322f..86d1466 100644 --- a/plugin/providers/phpipam/provider.go +++ b/plugin/providers/phpipam/provider.go @@ -40,12 +40,14 @@ func Provider() terraform.ResourceProvider { "phpipam_section": resourcePHPIPAMSection(), "phpipam_subnet": resourcePHPIPAMSubnet(), "phpipam_vlan": resourcePHPIPAMVLAN(), + "phpipam_first_free_subnet": resourcePHPIPAMFirstFreeSubnet(), }, DataSourcesMap: map[string]*schema.Resource{ "phpipam_address": dataSourcePHPIPAMAddress(), "phpipam_addresses": dataSourcePHPIPAMAddresses(), "phpipam_first_free_address": dataSourcePHPIPAMFirstFreeAddress(), + "phpipam_first_free_subnet": dataSourcePHPIPAMFirstFreeSubnet(), "phpipam_section": dataSourcePHPIPAMSection(), "phpipam_subnet": dataSourcePHPIPAMSubnet(), "phpipam_subnets": dataSourcePHPIPAMSubnets(), diff --git a/plugin/providers/phpipam/resource_phpipam_first_free_subnet.go b/plugin/providers/phpipam/resource_phpipam_first_free_subnet.go new file mode 100755 index 0000000..68add78 --- /dev/null +++ b/plugin/providers/phpipam/resource_phpipam_first_free_subnet.go @@ -0,0 +1,58 @@ +package phpipam + +import ( + "fmt" + "strings" + "strconv" + "errors" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourcePHPIPAMSubnet returns the resource structure for the phpipam_subnet +// 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 resourcePHPIPAMFirstFreeSubnet() *schema.Resource { + return &schema.Resource{ + Create: resourcePHPIPAMFirstFreeSubnetCreate, + Read: dataSourcePHPIPAMSubnetRead, + Update: resourcePHPIPAMSubnetUpdate, + Delete: resourcePHPIPAMSubnetDelete, + Schema: resourceFirstFreeSubnetSchema(), + } +} + +func resourcePHPIPAMFirstFreeSubnetCreate(d *schema.ResourceData, meta interface{}) error { + c := meta.(*ProviderPHPIPAMClient).subnetsController + + id := d.Get("master_subnet_id").(int) + mask := d.Get("subnet_mask").(int) + + message, err := c.CreateFirstFreeSubnet(id,mask); + if err != nil { + return err + } + cidr_mask := strings.Split(message, "/"); + d.Set("subnet_address", cidr_mask[0]) + if customFields, ok := d.GetOk("custom_fields"); ok { + subnets, err := c.GetSubnetsByCIDR(fmt.Sprintf("%s/%s", cidr_mask[0], cidr_mask[1])) + + if err != nil { + return fmt.Errorf("Could not read subnet after creating: %s", err) + } + + if len(subnets) != 1 { + return errors.New("Subnet either missing or multiple results returned by reading subnet after creation") + } + + d.SetId(strconv.Itoa(subnets[0].ID)) + d.Set("subnet_id", subnets[0].ID) + d.Set("subnet_address", subnets[0].SubnetAddress) + d.Set("subnet_mask", subnets[0].Mask) + if _, err := c.UpdateSubnetCustomFields(subnets[0].ID, customFields.(map[string]interface{})); err != nil { + return err + } + } + return dataSourcePHPIPAMSubnetRead(d, meta) +} \ No newline at end of file diff --git a/plugin/providers/phpipam/subnet_structure.go b/plugin/providers/phpipam/subnet_structure.go index ee59bab..bc3fc20 100644 --- a/plugin/providers/phpipam/subnet_structure.go +++ b/plugin/providers/phpipam/subnet_structure.go @@ -114,6 +114,31 @@ func bareSubnetSchema() map[string]*schema.Schema { } } +// resourceSubnetSchema returns the schema for the phpipam_first_free_subnet resource. It +// sets the required and optional fields, the latter defined in +// resourceSubnetRequiredFields, and ensures that all optional and +// non-configurable fields are computed as well. +func resourceFirstFreeSubnetSchema() map[string]*schema.Schema { + schema := bareSubnetSchema() + for k, v := range schema { + switch { + // Subnet Address and Mask are currently ForceNew + case k == "master_subnet_id" || k == "subnet_mask" : + v.Required = true + v.ForceNew = true + case k == "custom_fields": + v.Optional = true + case resourceSubnetOptionalFields.Has(k): + v.Optional = true + v.Computed = true + default: + v.Computed = true + } + } + return schema +} + + // resourceSubnetSchema returns the schema for the phpipam_subnet resource. It // sets the required and optional fields, the latter defined in // resourceSubnetRequiredFields, and ensures that all optional and diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go index 9f972a3..367bd1a 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/controllers/subnets/subnets.go @@ -4,7 +4,6 @@ package subnets import ( "fmt" - "github.com/paybyphone/phpipam-sdk-go/controllers/addresses" "github.com/paybyphone/phpipam-sdk-go/phpipam" "github.com/paybyphone/phpipam-sdk-go/phpipam/client" @@ -122,7 +121,19 @@ func (c *Controller) GetSubnetsByCIDR(cidr string) (out []Subnet, err error) { return } -// GetFirstFreeAddress GETs the first free IP address in a subnet and returns +// Create new child subnet inside subnet with specified mask. +func (c *Controller) CreateFirstFreeSubnet(id, mask int) (out string, err error) { + err = c.SendRequest("POST", fmt.Sprintf("/subnets/%d/first_subnet/%d", id, mask),&struct{}{}, &out) + return +} + +// Get first free/available subnet from master subnet +func (c *Controller) GetFirstFreeSubnet(id, mask int) (out string, err error) { + err = c.SendRequest("GET", fmt.Sprintf("/subnets/%d/first_subnet/%d", id, mask),&struct{}{}, &out) + return +} + +// GetFirstFreeAddress GETs the free IP address in a subnet and returns // it as a string. This can be used to automatically determine the next address // you should use. If there are no more available addresses, the string will be // blank. diff --git a/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/request/request.go b/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/request/request.go index d8f47dd..a6258ea 100644 --- a/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/request/request.go +++ b/vendor/github.com/paybyphone/phpipam-sdk-go/phpipam/request/request.go @@ -28,6 +28,21 @@ type APIResponse struct { Success bool } +type APIResponseInt struct { + // The HTTP result code. + Code int + + // The response data. This is further unmarshaled into the data type set by + // Request.Output. + Data json.RawMessage + + // The error message, if the request failed. + Message string + + // Whether or not the API request was successful. + Success int +} + // Request represents the API request. type Request struct { // The API session. @@ -76,10 +91,18 @@ func (r *requestResponse) BodyString() string { // request is successful and the response data is unmarshalled. func (r *requestResponse) ReadResponseJSON(v interface{}) error { var resp APIResponse + var respi APIResponseInt if err := json.Unmarshal(r.Body, &resp); err != nil { - return fmt.Errorf("JSON parsing error: %s - Response body: %s", err, r.Body) - } + if err := json.Unmarshal(r.Body, &respi); err != nil { + return fmt.Errorf("JSON parsing error: %s - Response body: %s", err, r.Body) + } else { + if (respi.Success == 0) { resp.Success = true } else { resp.Success = false } + resp.Code = respi.Code + resp.Data = respi.Data + resp.Message = respi.Message + } + } if !resp.Success { return r.handleError() }