From c7b06064c7ce936780b85c804666c3327a7c083e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Hagberg?= Date: Wed, 30 Oct 2024 16:30:35 +0100 Subject: [PATCH] A new feature lets you have multiple ips per host --- .goreleaser.yml | 3 +- examples/main.tf | 28 +++++- examples/run_example.sh | 4 +- internal/provider/resource_hosts.go | 144 ++++++++++++++++------------ 4 files changed, 112 insertions(+), 67 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 77d6355..fd3713e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,6 @@ # Visit https://goreleaser.com for documentation on how to customize this # behavior. +version: 1 before: hooks: # this is just an example and not a requirement for provider building/publishing @@ -38,7 +39,7 @@ checksum: signs: - artifacts: checksum args: - # if you are using this in a GitHub action or some other automated pipeline, you + # if you are using this in a GitHub action or some other automated pipeline, you # need to pass the batch flag to indicate its not interactive. - "--batch" - "--local-user" diff --git a/examples/main.tf b/examples/main.tf index fea8566..5bb440b 100644 --- a/examples/main.tf +++ b/examples/main.tf @@ -1,14 +1,14 @@ terraform { required_providers { mreg = { - version = "0.1.5" + version = "0.1.6" source = "uio.no/usit/mreg" } } } provider "mreg" { - serverurl = "https://mreg-test01.example.com/" + serverurl = "https://mreg-test.example.com/" token = "1234567890ABCDEF" # replace with actual token } @@ -20,14 +20,14 @@ resource "mreg_hosts" "my_hosts" { host { name = "terraform-provider-test02.example.com" # You can also manually pick an ip address instead of getting assigned a free one - manual_ipaddress = "192.0.2.55" + manual_ipaddress = "192.168.0.55" } host { name = "terraform-provider-test03.example.com" } contact = "your.email.address@example.com" comment = "Created by the Terraform provider for Mreg" - network = "192.0.2.0/24" + network = "192.168.0.0/16" policies = "without_monitoring, backup_no_backup" } @@ -43,7 +43,7 @@ resource "mreg_hosts" "loop_hosts" { } contact = "your.email.address@example.com" comment = "Created by the Terraform provider for Mreg" - network = "192.0.2.0/24" + network = "192.168.0.0/16" } resource "mreg_hosts" "metahosts" { @@ -71,6 +71,20 @@ resource "mreg_dns_srv" "srv" { port = 3306 } +# hosts with both IPv4 and IPv6 +resource "mreg_hosts" "host_with_multiple_ips" { + host { + name = "terraform-provider-test04.example.com" + } + host { + name = "terraform-provider-test05.example.com" + manual_ipaddress = "192.168.0.243,fd12:3456:789a:1::1" # the manual_ipaddress field can have a comma-separated list of addresses + } + contact = "your.email.address@example.com" + comment = "Created by the Terraform provider for Mreg" + network = "192.168.0.0/16,fc00::/7" # the network field can have a comma-separated list of networks +} + output "foo" { value = mreg_hosts.my_hosts } @@ -82,3 +96,7 @@ output "bar" { output "baz" { value = mreg_dns_srv.srv } + +output "qux" { + value = mreg_hosts.host_with_multiple_ips +} diff --git a/examples/run_example.sh b/examples/run_example.sh index 2f2be23..e19b493 100755 --- a/examples/run_example.sh +++ b/examples/run_example.sh @@ -3,14 +3,14 @@ set -e cd `dirname $0` pushd .. >/dev/null rm -f terraform-provider-mreg -go get -v +go get go build #TODO after the provider is added to the registry, there's no need to copy the file here rm -rf ~/.terraform.d/plugins/uio.no/usit/mreg/ mkdir -p ~/.terraform.d/plugins/uio.no/usit/mreg/0.1.5/linux_amd64 cp terraform-provider-mreg ~/.terraform.d/plugins/uio.no/usit/mreg/0.1.5/linux_amd64/ popd >/dev/null -rm -rf .terraform .terraform.lock.hcl terraform.tfstate crash.log +rm -rf .terraform .terraform.lock.hcl terraform.tfstate* crash.log terraform init terraform apply -auto-approve -parallelism=10 terraform plan -detailed-exitcode # If there's a diff, the provider is not refreshing the state correctly diff --git a/internal/provider/resource_hosts.go b/internal/provider/resource_hosts.go index 979b738..4e0e384 100644 --- a/internal/provider/resource_hosts.go +++ b/internal/provider/resource_hosts.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" "sort" - "strconv" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -76,6 +75,17 @@ func resourceHosts() *schema.Resource { } } +func splitString(source string) []string { + result := make([]string, 0) + for _, s := range strings.Split(source, ",") { + s = strings.TrimSpace(s) + if s != "" { + result = append(result, s) + } + } + return result +} + func resourceHostsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics @@ -84,17 +94,8 @@ func resourceHostsCreate(ctx context.Context, d *schema.ResourceData, m interfac hosts := d.Get("host").([]interface{}) comment := d.Get("comment").(string) contact := d.Get("contact").(string) - network := d.Get("network").(string) - policies_string := d.Get("policies").(string) - policies := make([]string, 0) - if policies_string != "" { - for _, s := range strings.Split(policies_string, ",") { - s = strings.TrimSpace(s) - if s != "" { - policies = append(policies, s) - } - } - } + networks := splitString(d.Get("network").(string)) + policies := splitString(d.Get("policies").(string)) lock := fslock.New("terraform-provider-mreg-lockfile") lock.Lock() @@ -106,24 +107,7 @@ func resourceHostsCreate(ctx context.Context, d *schema.ResourceData, m interfac hostname := host["name"].(string) hostnames[i] = hostname - var ipaddress string - - manual_ip := host["manual_ipaddress"].(string) - if manual_ip != "" { - ipaddress = manual_ip - } else { - if network != "" { - // Find a free IP address in Mreg - body, _, diags := apiClient.httpRequest( - "GET", fmt.Sprintf("/api/v1/networks/%s/first_unused", url.QueryEscape(network)), - nil, http.StatusOK) - if len(diags) > 0 { - return diags - } - - ipaddress = strings.Trim(body, "\"") - } - } + manual_ips := splitString(host["manual_ipaddress"].(string)) // Allocate a new host object in Mreg postdata := map[string]interface{}{ @@ -131,15 +115,54 @@ func resourceHostsCreate(ctx context.Context, d *schema.ResourceData, m interfac "contact": contact, "comment": comment, } - // Only add the ipaddress parameter if the host is supposed to have an IP address, or it will fail - if ipaddress != "" { - postdata["ipaddress"] = ipaddress + if len(manual_ips) > 0 { + postdata["ipaddress"] = manual_ips[0] + } else if len(networks) > 0 { + postdata["network"] = networks[0] } _, _, diags := apiClient.httpRequest("POST", "/api/v1/hosts/", postdata, http.StatusCreated) if len(diags) > 0 { return diags } + // Retrieve the host by name to find Mreg's internal ID for the host + _, body, diags := apiClient.httpRequest("GET", "/api/v1/hosts/"+url.QueryEscape(hostname), nil, http.StatusOK) + if len(diags) > 0 { + return diags + } + result := body.(map[string]interface{}) + host_id := int(result["id"].(float64)) + + // Assign any additional ip addresses + for i := 1; i < len(manual_ips); i++ { + postdata := map[string]interface{}{ + "ipaddress": manual_ips[i], + "host": host_id, + } + _, _, diags := apiClient.httpRequest("POST", "/api/v1/ipaddresses/", postdata, http.StatusCreated) + if len(diags) > 0 { + return diags + } + } + if len(manual_ips) == 0 { + for i := 1; i < len(networks); i++ { + // Find an unused address + _, body, diags := apiClient.httpRequest("GET", "/api/v1/networks/"+networks[i]+"/first_unused", nil, http.StatusOK) + if len(diags) > 0 { + return diags + } + ipaddress := body.(string) + postdata := map[string]interface{}{ + "ipaddress": ipaddress, + "host": host_id, + } + _, _, diags = apiClient.httpRequest("POST", "/api/v1/ipaddresses/", postdata, http.StatusCreated) + if len(diags) > 0 { + return diags + } + } + } + // Assign host policies, if any for _, p := range policies { postdata := map[string]interface{}{ @@ -151,8 +174,24 @@ func resourceHostsCreate(ctx context.Context, d *schema.ResourceData, m interfac } } + // Retrieve information about the host to find out which ip addresses it ended up with + _, body, diags = apiClient.httpRequest("GET", "/api/v1/hosts/"+url.QueryEscape(hostname), nil, http.StatusOK) + if len(diags) > 0 { + return diags + } + result = body.(map[string]interface{}) + ipaddressesCommaSeparated := "" + for _, elem := range result["ipaddresses"].([]interface{}) { + m := elem.(map[string]interface{}) + if ipaddressesCommaSeparated == "" { + ipaddressesCommaSeparated = m["ipaddress"].(string) + } else { + ipaddressesCommaSeparated = ipaddressesCommaSeparated + "," + m["ipaddress"].(string) + } + } + // Update the ResourceData - host["ipaddress"] = ipaddress + host["ipaddress"] = ipaddressesCommaSeparated host["comment"] = comment host["contact"] = contact hosts[i] = host @@ -194,9 +233,20 @@ func resourceHostsRead(ctx context.Context, d *schema.ResourceData, m interface{ } result := body.(map[string]interface{}) + // make a comma-separated list of the IP address(es) in case there are many + ipaddressesCommaSeparated := "" + for _, elem := range result["ipaddresses"].([]interface{}) { + m := elem.(map[string]interface{}) + if ipaddressesCommaSeparated == "" { + ipaddressesCommaSeparated = m["ipaddress"].(string) + } else { + ipaddressesCommaSeparated = ipaddressesCommaSeparated + "," + m["ipaddress"].(string) + } + } + // Update the data model with data from Mreg host["comment"] = result["comment"] - host["ipaddress"] = GetStringFromData(result, "ipaddresses.0.ipaddress") + host["ipaddress"] = ipaddressesCommaSeparated host["contact"] = result["contact"] hosts[i] = host @@ -243,27 +293,3 @@ func compoundId(hostnames []string) string { } return fmt.Sprintf("%x", hash.Sum(nil)) } - -// GetStringFromData lets you specify a path to the value that you want -// (e.g. "aaa.bbb.ccc") and have it extracted from the data structure. -func GetStringFromData(v interface{}, path string) string { - for _, key := range strings.Split(path, ".") { - iKey, err := strconv.ParseInt(key, 10, 32) - if err == nil { - // If the key is a number, we assume the structure is an array - arr, ok := v.([]interface{}) - if !ok || int64(len(arr)) <= iKey { - return "" - } - v = arr[iKey] - } else { - // If the key isn't a number, we assume the structure is a map - m, ok := v.(map[string]interface{}) - if !ok { - return "" - } - v = m[key] - } - } - return fmt.Sprintf("%v", v) -}