diff --git a/libvirt/data_source_libvirt_network.go b/libvirt/data_source_libvirt_network.go new file mode 100644 index 000000000..9ef7e843b --- /dev/null +++ b/libvirt/data_source_libvirt_network.go @@ -0,0 +1,69 @@ +package libvirt + +import ( + "fmt" + "net" + "strconv" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +// a libvirt network DNS host template datasource +// +// Datasource example: +// +// data "libvirt_network_dns_host_template" "k8smasters" { +// count = "${var.master_count}" +// ip = "${var.master_ips[count.index]}" +// hostname = "master-${count.index}" +// } +// +// resource "libvirt_network" "k8snet" { +// ... +// dns = [{ +// hosts = [ "${flatten(data.libvirt_network_dns_host_template.k8smasters.*.rendered)}" ] +// }] +// ... +// } +// +func datasourceLibvirtNetworkDNSHostTemplate() *schema.Resource { + return &schema.Resource{ + Read: resourceLibvirtNetworkDNSHostRead, + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + }, + "hostname": { + Type: schema.TypeString, + Required: true, + }, + "rendered": { + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + }, + } +} + +func resourceLibvirtNetworkDNSHostRead(d *schema.ResourceData, meta interface{}) error { + dnsHost := map[string]interface{}{} + if address, ok := d.GetOk("ip"); ok { + ip := net.ParseIP(address.(string)) + if ip == nil { + return fmt.Errorf("Could not parse address '%s'", address) + } + dnsHost["ip"] = ip.String() + } + if hostname, ok := d.GetOk("hostname"); ok { + dnsHost["hostname"] = hostname.(string) + } + d.Set("rendered", dnsHost) + d.SetId(strconv.Itoa(hashcode.String(fmt.Sprintf("%v", dnsHost)))) + + return nil +} diff --git a/libvirt/data_source_libvirt_network_test.go b/libvirt/data_source_libvirt_network_test.go new file mode 100644 index 000000000..3b1b921a7 --- /dev/null +++ b/libvirt/data_source_libvirt_network_test.go @@ -0,0 +1,53 @@ +package libvirt + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccLibvirtNetworkDataSource_DNSHostTemplate(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtNetworkDestroy, + Steps: []resource.TestStep{ + + { + Config: `data "libvirt_network_dns_host_template" "bootstrap" { + count = 2 + ip = "1.1.1.${count.index}" + hostname = "myhost${count.index}" +}`, + Check: resource.ComposeTestCheckFunc( + checkDNSHostTemplate("data.libvirt_network_dns_host_template.bootstrap.0", "ip", "1.1.1.0"), + checkDNSHostTemplate("data.libvirt_network_dns_host_template.bootstrap.0", "hostname", "myhost0"), + checkDNSHostTemplate("data.libvirt_network_dns_host_template.bootstrap.1", "ip", "1.1.1.1"), + checkDNSHostTemplate("data.libvirt_network_dns_host_template.bootstrap.1", "hostname", "myhost1"), + ), + }, + }, + }) +} + +func checkDNSHostTemplate(id, name, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[id] + if !ok { + return fmt.Errorf("Not found: %s", id) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + v := rs.Primary.Attributes[name] + if v != value { + return fmt.Errorf( + "Value for %s is %s, not %s", name, v, value) + } + + return nil + } +} diff --git a/libvirt/provider.go b/libvirt/provider.go index 1d16b9320..7d314a2e1 100644 --- a/libvirt/provider.go +++ b/libvirt/provider.go @@ -27,6 +27,10 @@ func Provider() terraform.ResourceProvider { "libvirt_ignition": resourceIgnition(), }, + DataSourcesMap: map[string]*schema.Resource{ + "libvirt_network_dns_host_template": datasourceLibvirtNetworkDNSHostTemplate(), + }, + ConfigureFunc: providerConfigure, } } diff --git a/libvirt/resource_libvirt_network.go b/libvirt/resource_libvirt_network.go index 4e7fabc3a..bfb6f6678 100644 --- a/libvirt/resource_libvirt_network.go +++ b/libvirt/resource_libvirt_network.go @@ -115,6 +115,31 @@ func resourceLibvirtNetwork() *schema.Resource { }, }, }, + "hosts": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + // This should be required, but Terraform does validation too early + // and therefore doesn't recognize that this is set when assigning from + // a rendered dns_host template. + Optional: true, + ForceNew: true, + }, + "hostname": { + Type: schema.TypeString, + // This should be required, but Terraform does validation too early + // and therefore doesn't recognize that this is set when assigning from + // a rendered dns_host template. + Optional: true, + ForceNew: true, + }, + }, + }, + }, }, }, }, @@ -243,11 +268,8 @@ func resourceLibvirtNetworkCreate(d *schema.ResourceData, meta interface{}) erro if err != nil { return fmt.Errorf("Could not set DHCP from adresses '%s'", err) } + var dnsForwarders []libvirtxml.NetworkDNSForwarder if dnsForwardCount, ok := d.GetOk(dnsPrefix + ".forwarders.#"); ok { - dns := libvirtxml.NetworkDNS{ - Forwarders: []libvirtxml.NetworkDNSForwarder{}, - } - for i := 0; i < dnsForwardCount.(int); i++ { forward := libvirtxml.NetworkDNSForwarder{} forwardPrefix := fmt.Sprintf(dnsPrefix+".forwarders.%d", i) @@ -261,7 +283,40 @@ func resourceLibvirtNetworkCreate(d *schema.ResourceData, meta interface{}) erro if domain, ok := d.GetOk(forwardPrefix + ".domain"); ok { forward.Domain = domain.(string) } - dns.Forwarders = append(dns.Forwarders, forward) + dnsForwarders = append(dnsForwarders, forward) + } + } + + dnsHostsMap := map[string][]string{} + if dnsHostCount, ok := d.GetOk(dnsPrefix + ".hosts.#"); ok { + for i := 0; i < dnsHostCount.(int); i++ { + hostPrefix := fmt.Sprintf(dnsPrefix+".hosts.%d", i) + + address := d.Get(hostPrefix + ".ip").(string) + if net.ParseIP(address) == nil { + return fmt.Errorf("Could not parse address '%s'", address) + } + + dnsHostsMap[address] = append(dnsHostsMap[address], d.Get(hostPrefix+".hostname").(string)) + } + } + + var dnsHosts []libvirtxml.NetworkDNSHost + for ip, hostnames := range dnsHostsMap { + dnsHostnames := []libvirtxml.NetworkDNSHostHostname{} + for _, hostname := range hostnames { + dnsHostnames = append(dnsHostnames, libvirtxml.NetworkDNSHostHostname{Hostname: hostname}) + } + dnsHosts = append(dnsHosts, libvirtxml.NetworkDNSHost{ + IP: ip, + Hostnames: dnsHostnames, + }) + } + + if len(dnsForwarders) > 0 || len(dnsHosts) > 0 { + dns := libvirtxml.NetworkDNS{ + Forwarders: dnsForwarders, + Host: dnsHosts, } networkDef.DNS = &dns } diff --git a/libvirt/resource_libvirt_network_test.go b/libvirt/resource_libvirt_network_test.go index c23806177..12186bce9 100644 --- a/libvirt/resource_libvirt_network_test.go +++ b/libvirt/resource_libvirt_network_test.go @@ -156,6 +156,92 @@ func checkDNSForwarders(name string, expected []libvirtxml.NetworkDNSForwarder) } } +func TestAccLibvirtNetwork_DNSHosts(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtNetworkDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_network" "test_net" { + name = "networktest" + domain = "k8s.local" + addresses = ["10.17.3.0/24"] + dns { + hosts = [ + { + hostname = "myhost1", + ip = "1.1.1.1", + }, + { + hostname = "myhost1", + ip = "1.1.1.2", + }, + { + hostname = "myhost2", + ip = "1.1.1.1", + }, + ] + } + }`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.hosts.0.hostname", "myhost1"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.hosts.0.ip", "1.1.1.1"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.hosts.1.hostname", "myhost1"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.hosts.1.ip", "1.1.1.2"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.hosts.2.hostname", "myhost2"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.hosts.2.ip", "1.1.1.1"), + checkDNSHosts("libvirt_network.test_net", []libvirtxml.NetworkDNSHost{ + { + IP: "1.1.1.1", + Hostnames: []libvirtxml.NetworkDNSHostHostname{ + {Hostname: "myhost1"}, + {Hostname: "myhost2"}, + }, + }, + { + IP: "1.1.1.2", + Hostnames: []libvirtxml.NetworkDNSHostHostname{ + {Hostname: "myhost1"}, + }, + }, + }), + ), + }, + }, + }) +} + +func checkDNSHosts(name string, expected []libvirtxml.NetworkDNSHost) resource.TestCheckFunc { + return func(s *terraform.State) error { + networkDef, err := getNetworkDef(s, name) + if err != nil { + return err + } + if networkDef.DNS == nil { + return fmt.Errorf("DNS block not found in networkDef") + } + actual := networkDef.DNS.Host + if len(expected) != len(actual) { + return fmt.Errorf("len(expected): %d != len(actual): %d", len(expected), len(actual)) + } + for _, e := range expected { + found := false + for _, a := range actual { + if reflect.DeepEqual(a.IP, e.IP) && reflect.DeepEqual(a.Hostnames, e.Hostnames) { + found = true + break + } + } + if !found { + return fmt.Errorf("Unable to find:%v in: %v", e, actual) + } + } + return nil + } +} + func networkExists(n string, network *libvirt.Network) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] diff --git a/website/docs/r/network.markdown b/website/docs/r/network.markdown index 2642f16ac..c2403f26f 100644 --- a/website/docs/r/network.markdown +++ b/website/docs/r/network.markdown @@ -52,6 +52,19 @@ resource "libvirt_network" "kube_network" { # domain = "my domain" # } # ] + + # (Optional) one or more DNS host entries. Both of + # "ip" and "hostname" must be specified. The format is: + # hosts = [ + # { + # hostname = "my_hostname" + # ip = "my.ip.address.1" + # }, + # { + # hostname = "my_hostname" + # ip = "my.ip.address.2" + # }, + # ] } } ``` @@ -96,7 +109,26 @@ The following arguments are supported: Inside of `dns` section the following argument are supported: * `local_only` - (Optional) true/false: true means 'do not forward unresolved requests for this domain to the part DNS server * `forwarders` - (Optional) Either `address`, `domain`, or both must be set +* `hosts` - (Optional) a DNS host entry block. You can have one or more of these + blocks in your DNS definition. You must specify both `ip` and `hostname`. + +An advanced example of round-robin DNS (using DNS host templates) follows: +```hcl +resource "libvirt_network" "my_network" { + ... + dns = { + hosts = [ "${flatten(data.libvirt_network_dns_host_template.hosts.*.rendered)}" ] + } + ... +} + +data "libvirt_network_dns_host_template" "hosts" { + count = "${var.host_count}" + ip = "${var.host_ips[count.index]}" + hostname = "my_host" +} +``` * `dhcp` - (Optional) DHCP configuration. You need to use it in conjuction with the adresses variable.