Skip to content

Commit

Permalink
Add support for DNS hosts
Browse files Browse the repository at this point in the history
The DNS hosts feature allows DNS records to be created that point to a
given IP address. This is useful for aliasing as well as setting up
round-robin DNS (by creating multiple hosts that point to the same IP
address).

This creates a new datasource called libvirt_network_dns_hosts_template,
which can be used to populate the dns_host attribute in libvirt_network
resources. The use of a datasource is required in order to dynamically
generate dns_host entries (since 'count' is the only looping construct
in Terraform and 'count' is limited to datasources and resources). The
list of hostnames has also been replaced by a single hostname with the
option of declaring multiple dns_host blocks with the same IP address.

I originally tried to externally generate a variable that would host the
list of maps needed for the dns_host attribute, but Terraform explicitly
disallows lists of maps greater than length one. Instead, I decided to
follow the pattern established by template_file and create a resource
that can be rendered.

The hostnames attribute had to be replaced with hostname because the
underlying schema for the 'rendered' attribute is a TypeMap. In
Terraform, this is not a generic map, but is instead limited to
map[string]string. To accommodate this, multiple dns_host blocks can be
defined with the same IP address, one for each hostname.
  • Loading branch information
crawford authored and eparis committed Sep 5, 2018
1 parent ad5391e commit a4d0ba6
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 5 deletions.
69 changes: 69 additions & 0 deletions libvirt/data_source_libvirt_network.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions libvirt/data_source_libvirt_network_test.go
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 4 additions & 0 deletions libvirt/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func Provider() terraform.ResourceProvider {
"libvirt_ignition": resourceIgnition(),
},

DataSourcesMap: map[string]*schema.Resource{
"libvirt_network_dns_host_template": datasourceLibvirtNetworkDNSHostTemplate(),
},

ConfigureFunc: providerConfigure,
}
}
Expand Down
65 changes: 60 additions & 5 deletions libvirt/resource_libvirt_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
},
},
},
},
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
86 changes: 86 additions & 0 deletions libvirt/resource_libvirt_network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
32 changes: 32 additions & 0 deletions website/docs/r/network.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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"
# },
# ]
}
}
```
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit a4d0ba6

Please sign in to comment.