diff options
author | Alvaro Saurin <alvaro.saurin@gmail.com> | 2016-06-27 12:06:29 +0200 |
---|---|---|
committer | Alvaro Saurin <alvaro.saurin@gmail.com> | 2016-07-01 22:34:40 +0200 |
commit | f6a859c2f91d3adef137270879e936271adaaf01 (patch) | |
tree | 147bdc7bf5fdd8f1a688a5f5a5927fec1063db74 /libvirt | |
parent | e9301b5a3a18c93f6a00bfad857ef0474b548928 (diff) | |
download | terraform-provider-libvirt-f6a859c2f91d3adef137270879e936271adaaf01.tar terraform-provider-libvirt-f6a859c2f91d3adef137270879e936271adaaf01.tar.gz |
Improved network resource
Methods for adding/removing hosts to a network
Style, formatting improvements and fixes
Diffstat (limited to 'libvirt')
-rw-r--r-- | libvirt/network_def.go | 145 | ||||
-rw-r--r-- | libvirt/network_def_test.go | 93 | ||||
-rw-r--r-- | libvirt/network_interface_def.go | 73 | ||||
-rw-r--r-- | libvirt/provider.go | 1 | ||||
-rw-r--r-- | libvirt/resource_libvirt_domain.go | 339 | ||||
-rw-r--r-- | libvirt/resource_libvirt_domain_netiface.go | 68 | ||||
-rw-r--r-- | libvirt/resource_libvirt_domain_test.go | 5 | ||||
-rw-r--r-- | libvirt/resource_libvirt_network.go | 331 | ||||
-rw-r--r-- | libvirt/utils.go | 32 | ||||
-rw-r--r-- | libvirt/utils_libvirt.go | 41 | ||||
-rw-r--r-- | libvirt/utils_net.go | 58 | ||||
-rw-r--r-- | libvirt/utils_test.go | 16 |
12 files changed, 1060 insertions, 142 deletions
diff --git a/libvirt/network_def.go b/libvirt/network_def.go new file mode 100644 index 00000000..eb263f08 --- /dev/null +++ b/libvirt/network_def.go @@ -0,0 +1,145 @@ +package libvirt + +import ( + "encoding/xml" + "fmt" + + libvirt "github.com/dmacvicar/libvirt-go" +) + +type defNetworkIpDhcpRange struct { + XMLName xml.Name `xml:"range,omitempty"` + + Start string `xml:"start,attr,omitempty"` + End string `xml:"end,attr,omitempty"` +} + +type defNetworkIpDhcpHost struct { + XMLName xml.Name `xml:"host,omitempty"` + + Ip string `xml:"ip,attr,omitempty"` + Mac string `xml:"mac,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` +} + +type defNetworkIpDhcp struct { + XMLName xml.Name `xml:"dhcp,omitempty"` + + Ranges []*defNetworkIpDhcpRange `xml:"range,omitempty"` + Hosts []*defNetworkIpDhcpHost `xml:"host,omitempty"` +} + +type defNetworkIp struct { + XMLName xml.Name `xml:"ip,omitempty"` + + Address string `xml:"address,attr"` + Netmask string `xml:"netmask,attr,omitempty"` + Prefix int `xml:"prefix,attr,omitempty"` + Family string `xml:"family,attr,omitempty"` + Dhcp *defNetworkIpDhcp `xml:"dhcp,omitempty"` +} + +type defNetworkBridge struct { + XMLName xml.Name `xml:"bridge,omitempty"` + + Name string `xml:"name,attr,omitempty"` + Stp string `xml:"stp,attr,omitempty"` +} + +type defNetworkDomain struct { + XMLName xml.Name `xml:"domain,omitempty"` + + Name string `xml:"name,attr,omitempty"` + LocalOnly string `xml:"localOnly,attr,omitempty"` +} + +type defNetworkForward struct { + Mode string `xml:"mode,attr"` + Device string `xml:"dev,attr,omitempty"` + Nat *struct { + Addresses []*struct { + Start string `xml:"start,attr"` + End string `xml:"end,attr"` + } `xml:"address,omitempty"` + Ports []*struct { + Start string `xml:"start,attr"` + End string `xml:"end,attr"` + } `xml:"port,omitempty"` + } `xml:"nat,omitempty"` +} + +type defNetworkDns struct { + Host []*struct { + Ip string `xml:"ip,attr"` + HostName []string `xml:"hostname"` + } `xml:"host,omitempty"` + Forwarder []*struct { + Address string `xml:"addr,attr"` + } `xml:"forwarder,omitempty"` +} + +// network definition in XML, compatible with what libvirt expects +// note: we have to use pointers or otherwise golang's XML will not properly detect +// empty values and generate things like "<bridge></bridge>" that +// make libvirt crazy... +type defNetwork struct { + XMLName xml.Name `xml:"network"` + + Name string `xml:"name,omitempty"` + Domain *defNetworkDomain `xml:"domain,omitempty"` + Bridge *defNetworkBridge `xml:"bridge,omitempty"` + Forward *defNetworkForward `xml:"forward,omitempty"` + Ips []*defNetworkIp `xml:"ip,omitempty"` + Dns *defNetworkDns `xml:"dns,omitempty"` +} + +// Check if the network has a DHCP server managed by libvirt +func (net defNetwork) HasDHCP() bool { + if net.Forward != nil { + if net.Forward.Mode == "nat" || net.Forward.Mode == "route" || net.Forward.Mode == "" { + return true + } + } + return false +} + +// Creates a network definition from a XML +func newDefNetworkFromXML(s string) (defNetwork, error) { + var networkDef defNetwork + err := xml.Unmarshal([]byte(s), &networkDef) + if err != nil { + return defNetwork{}, err + } + return networkDef, nil +} + +func newDefNetworkfromLibvirt(network *libvirt.VirNetwork) (defNetwork, error) { + networkXmlDesc, err := network.GetXMLDesc(0) + if err != nil { + return defNetwork{}, fmt.Errorf("Error retrieving libvirt domain XML description: %s", err) + } + networkDef := defNetwork{} + err = xml.Unmarshal([]byte(networkXmlDesc), &networkDef) + if err != nil { + return defNetwork{}, fmt.Errorf("Error reading libvirt network XML description: %s", err) + } + return networkDef, nil +} + +// Creates a network definition with the defaults the provider uses +func newNetworkDef() defNetwork { + const defNetworkXML = ` + <network> + <name>default</name> + <forward mode='nat'> + <nat> + <port start='1024' end='65535'/> + </nat> + </forward> + </network>` + if d, err := newDefNetworkFromXML(defNetworkXML); err != nil { + panic(fmt.Sprint("Unexpected error while parsing default network definition: %s", err)) + } else { + return d + } +} diff --git a/libvirt/network_def_test.go b/libvirt/network_def_test.go new file mode 100644 index 00000000..cd2048c7 --- /dev/null +++ b/libvirt/network_def_test.go @@ -0,0 +1,93 @@ +package libvirt + +import ( + "bytes" + "encoding/xml" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func init() { + spew.Config.Indent = "\t" +} + +func TestDefaultNetworkMarshall(t *testing.T) { + b := newNetworkDef() + prettyB := spew.Sdump(b) + t.Logf("Parsed default network:\n%s", prettyB) + + buf := new(bytes.Buffer) + enc := xml.NewEncoder(buf) + enc.Indent(" ", " ") + if err := enc.Encode(b); err != nil { + t.Fatalf("could not marshall this:\n%s", spew.Sdump(b)) + } + t.Logf("Marshalled default network:\n%s", buf.String()) +} + +func TestNetworkDefUnmarshall(t *testing.T) { + // some testing XML from the official docs (some unsupported attrs will be just ignored) + text := ` + <network> + <name>my-network</name> + <bridge name="virbr0" stp="on" delay="5" macTableManager="libvirt"/> + <mac address='00:16:3E:5D:C7:9E'/> + <domain name="example.com" localOnly="no"/> + <forward mode='nat'> + <nat> + <address start='1.2.3.4' end='1.2.3.10'/> + </nat> + </forward> + <dns> + <txt name="example" value="example value" /> + <forwarder addr="8.8.8.8"/> + <forwarder addr="8.8.4.4"/> + <srv service='name' protocol='tcp' domain='test-domain-name' target='.' port='1024' priority='10' weight='10'/> + <host ip='192.168.122.2'> + <hostname>myhost</hostname> + <hostname>myhostalias</hostname> + </host> + </dns> + <ip address="192.168.122.1" netmask="255.255.255.0"> + <dhcp> + <range start="192.168.122.100" end="192.168.122.254" /> + <host mac="00:16:3e:77:e2:ed" name="foo.example.com" ip="192.168.122.10" /> + <host mac="00:16:3e:3e:a9:1a" name="bar.example.com" ip="192.168.122.11" /> + </dhcp> + </ip> + <ip family="ipv6" address="2001:db8:ca2:2::1" prefix="64" /> + <route family="ipv6" address="2001:db9:ca1:1::" prefix="64" gateway="2001:db8:ca2:2::2" /> + </network> + ` + + b, err := newDefNetworkFromXML(text) + prettyB := spew.Sdump(b) + t.Logf("Parsed:\n%s", prettyB) + if err != nil { + t.Errorf("could not parse: %s", err) + } + if b.Name != "my-network" { + t.Errorf("wrong network name: '%s'", b.Name) + } + if b.Domain.Name != "example.com" { + t.Errorf("wrong domain name: '%s'", b.Domain.Name) + } + if b.Forward.Mode != "nat" { + t.Errorf("wrong forward mode: '%s'", b.Forward.Mode) + } + if len(b.Forward.Nat.Addresses) == 0 { + t.Errorf("wrong number of addresses: %s", b.Forward.Nat.Addresses) + } + if b.Forward.Nat.Addresses[0].Start != "1.2.3.4" { + t.Errorf("wrong forward start address: %s", b.Forward.Nat.Addresses[0].Start) + } + if len(b.Ips) == 0 { + t.Errorf("wrong number of IPs: %d", len(b.Ips)) + } + if bs, err := xmlMarshallIndented(b); err != nil { + t.Fatalf("marshalling error\n%s", spew.Sdump(b)) + } else { + t.Logf("Marshalled:\n%s", bs) + } +} diff --git a/libvirt/network_interface_def.go b/libvirt/network_interface_def.go index 289211f7..20a4c248 100644 --- a/libvirt/network_interface_def.go +++ b/libvirt/network_interface_def.go @@ -2,9 +2,16 @@ package libvirt import ( "encoding/xml" - "github.com/hashicorp/terraform/helper/schema" ) +// An interface definition, as returned/understood by libvirt +// (see https://libvirt.org/formatdomain.html#elementsNICS) +// +// Something like: +// <interface type='network'> +// <source network='default'/> +// </interface> +// type defNetworkInterface struct { XMLName xml.Name `xml:"interface"` Type string `xml:"type,attr"` @@ -13,68 +20,12 @@ type defNetworkInterface struct { } `xml:"mac"` Source struct { Network string `xml:"network,attr"` - } `xml:"source"` + Bridge string `xml:"bridge,attr"` + Dev string `xml:"dev,attr"` + Mode string `xml:"mode,attr"` + } `xml:"source"` Model struct { Type string `xml:"type,attr"` } `xml:"model"` waitForLease bool } - -func networkAddressCommonSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "type": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "prefix": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Computed: true, - }, - } -} - -func networkInterfaceCommonSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "network": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "default", - ForceNew: true, - }, - "mac": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - "wait_for_lease": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - }, - "address": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: networkAddressCommonSchema(), - }, - }, - } -} - -func newDefNetworkInterface() defNetworkInterface { - iface := defNetworkInterface{} - iface.Type = "network" - //iface.Mac.Address = "52:54:00:36:c0:65" - iface.Source.Network = "default" - iface.Model.Type = "virtio" - iface.waitForLease = false - return iface -} diff --git a/libvirt/provider.go b/libvirt/provider.go index 5290816e..b0f86054 100644 --- a/libvirt/provider.go +++ b/libvirt/provider.go @@ -19,6 +19,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "libvirt_domain": resourceLibvirtDomain(), "libvirt_volume": resourceLibvirtVolume(), + "libvirt_network": resourceLibvirtNetwork(), "libvirt_cloudinit": resourceCloudInit(), }, diff --git a/libvirt/resource_libvirt_domain.go b/libvirt/resource_libvirt_domain.go index a0aff83e..67c95b38 100644 --- a/libvirt/resource_libvirt_domain.go +++ b/libvirt/resource_libvirt_domain.go @@ -3,18 +3,27 @@ package libvirt import ( "encoding/xml" "fmt" - libvirt "github.com/dmacvicar/libvirt-go" - "github.com/hashicorp/terraform/helper/schema" "log" + "net" + "strings" "time" + + "github.com/davecgh/go-spew/spew" + libvirt "github.com/dmacvicar/libvirt-go" + "github.com/hashicorp/terraform/helper/schema" ) +func init() { + spew.Config.Indent = "\t" +} + func resourceLibvirtDomain() *schema.Resource { return &schema.Resource{ Create: resourceLibvirtDomainCreate, Read: resourceLibvirtDomainRead, Delete: resourceLibvirtDomainDelete, Update: resourceLibvirtDomainUpdate, + Exists: resourceLibvirtDomainExists, Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, @@ -64,7 +73,6 @@ func resourceLibvirtDomain() *schema.Resource { Type: schema.TypeList, Optional: true, Required: false, - ForceNew: true, Elem: &schema.Resource{ Schema: networkInterfaceCommonSchema(), }, @@ -73,12 +81,34 @@ func resourceLibvirtDomain() *schema.Resource { } } +func resourceLibvirtDomainExists(d *schema.ResourceData, meta interface{}) (bool, error) { + virConn := meta.(*Client).libvirt + if virConn == nil { + return false, fmt.Errorf("The libvirt connection was nil.") + } + domain, err := virConn.LookupByUUIDString(d.Id()) + defer domain.Free() + return err == nil, err +} + func resourceLibvirtDomainCreate(d *schema.ResourceData, meta interface{}) error { virConn := meta.(*Client).libvirt if virConn == nil { return fmt.Errorf("The libvirt connection was nil.") } + domainDef := newDomainDef() + if name, ok := d.GetOk("name"); ok { + domainDef.Name = name.(string) + } + + if metadata, ok := d.GetOk("metadata"); ok { + domainDef.Metadata.TerraformLibvirt.Xml = metadata.(string) + } + + domainDef.Memory.Amount = d.Get("memory").(int) + domainDef.VCpu.Amount = d.Get("vcpu").(int) + disksCount := d.Get("disk.#").(int) var disks []defDisk for i := 0; i < disksCount; i++ { @@ -110,6 +140,12 @@ func resourceLibvirtDomainCreate(d *schema.ResourceData, meta interface{}) error disks = append(disks, disk) } + type pendingMapping struct { + mac string + hostname string + network *libvirt.VirNetwork + } + if cloudinit, ok := d.GetOk("cloudinit"); ok { disk, err := newDiskForCloudInit(virConn, cloudinit.(string)) if err != nil { @@ -120,42 +156,116 @@ func resourceLibvirtDomainCreate(d *schema.ResourceData, meta interface{}) error netIfacesCount := d.Get("network_interface.#").(int) netIfaces := make([]defNetworkInterface, 0, netIfacesCount) + partialNetIfaces := make(map[string]pendingMapping, netIfacesCount) for i := 0; i < netIfacesCount; i++ { prefix := fmt.Sprintf("network_interface.%d", i) - netIface := newDefNetworkInterface() - if mac, ok := d.GetOk(prefix + ".mac"); ok { - netIface.Mac.Address = mac.(string) - } else { + netIface := defNetworkInterface{} + netIface.Model.Type = "virtio" + + // calculate the MAC address + macI, ok := d.GetOk(prefix + ".mac") + mac := strings.ToUpper(macI.(string)) + if !ok { var err error - netIface.Mac.Address, err = RandomMACAddress() + mac, err = RandomMACAddress() if err != nil { return fmt.Errorf("Error generating mac address: %s", err) } } + netIface.Mac.Address = mac // this is not passed to libvirt, but used by waitForAddress + netIface.waitForLease = false if waitForLease, ok := d.GetOk(prefix + ".wait_for_lease"); ok { netIface.waitForLease = waitForLease.(bool) } - if network, ok := d.GetOk(prefix + ".network"); ok { - netIface.Source.Network = network.(string) - } - netIfaces = append(netIfaces, netIface) - } + // connect to the interface to the network... first, look for the network + if n, ok := d.GetOk(prefix + ".network_name"); ok { + // when using a "network_name" we do not try to do anything: we just + // connect to that network + netIface.Type = "network" + netIface.Source.Network = n.(string) + } else if networkUUID, ok := d.GetOk(prefix + ".network_id"); ok { + // when using a "network_id" we are referring to a "network resource" + // we have defined somewhere else... + network, err := virConn.LookupNetworkByUUIDString(networkUUID.(string)) + if err != nil { + return fmt.Errorf("Can't retrieve network ID %s", networkUUID) + } + defer network.Free() - domainDef := newDomainDef() - if name, ok := d.GetOk("name"); ok { - domainDef.Name = name.(string) - } + networkName, err := network.GetName() + if err != nil { + return fmt.Errorf("Error retrieving volume name: %s", err) + } + networkDef, err := newDefNetworkfromLibvirt(&network) + if !networkDef.HasDHCP() { + continue + } - if metadata, ok := d.GetOk("metadata"); ok { - domainDef.Metadata.TerraformLibvirt.Xml = metadata.(string) + hostname := domainDef.Name + if hostnameI, ok := d.GetOk(prefix + ".hostname"); ok { + hostname = hostnameI.(string) + } + if addresses, ok := d.GetOk("addresses"); ok { + // some IP(s) provided + for _, addressI := range addresses.([]interface{}) { + address := addressI.(string) + ip := net.ParseIP(address) + if ip == nil { + return fmt.Errorf("Could not parse addresses '%s'", address) + } + // TODO: we should check the IP is contained in the DHCP addresses served + log.Printf("[INFO] Adding IP/MAC/host=%s/%s/%s to %s", ip.String(), mac, hostname, networkName) + if err := addHost(&network, ip.String(), mac, hostname); err != nil { + return err + } + } + } else { + // no IPs provided: if the hostname has been provided, wait until we get an IP + if len(hostname) > 0 { + if !netIface.waitForLease { + return fmt.Errorf("Cannot map '%s': we are not waiting for lease and no IP has been provided", hostname) + } + // the resource specifies a hostname but not an IP, so we must wait until we + // have a valid lease and then read the IP we have been assigned, so we can + // do the mapping + log.Printf("[DEBUG] Will wait for an IP for hostname '%s'...", hostname) + partialNetIfaces[mac] = pendingMapping{ + mac: mac, + hostname: hostname, + network: &network, + } + } else { + // neither an IP or a hostname has been provided: so nothing must be forced + } + } + netIface.Type = "network" + netIface.Source.Network = networkName + } else if bridgeNameI, ok := d.GetOk(prefix + ".bridge"); ok { + netIface.Type = "bridge" + netIface.Source.Bridge = bridgeNameI.(string) + } else if devI, ok := d.GetOk(prefix + ".vepa"); ok { + netIface.Type = "direct" + netIface.Source.Dev = devI.(string) + netIface.Source.Mode = "vepa" + } else if devI, ok := d.GetOk(prefix + ".macvtap"); ok { + netIface.Type = "direct" + netIface.Source.Dev = devI.(string) + netIface.Source.Mode = "bridge" + } else if devI, ok := d.GetOk(prefix + ".passthrough"); ok { + netIface.Type = "direct" + netIface.Source.Dev = devI.(string) + netIface.Source.Mode = "passthrough" + } else { + // no network has been specified: we are on our own + } + + netIfaces = append(netIfaces, netIface) } - domainDef.Memory.Amount = d.Get("memory").(int) - domainDef.VCpu.Amount = d.Get("vcpu").(int) domainDef.Devices.Disks = disks domainDef.Devices.NetworkInterfaces = netIfaces @@ -165,11 +275,13 @@ func resourceLibvirtDomainCreate(d *schema.ResourceData, meta interface{}) error } log.Printf("[INFO] Creating libvirt domain at %s", connectURI) - data, err := xml.Marshal(domainDef) + data, err := xmlMarshallIndented(domainDef) if err != nil { return fmt.Errorf("Error serializing libvirt domain: %s", err) } + log.Printf("[DEBUG] Creating libvirt domain with XML:\n%s", string(data)) + domain, err := virConn.DomainDefineXML(string(data)) if err != nil { return fmt.Errorf("Error defining libvirt domain: %s", err) @@ -199,7 +311,37 @@ func resourceLibvirtDomainCreate(d *schema.ResourceData, meta interface{}) error return err } - return resourceLibvirtDomainRead(d, meta) + err = resourceLibvirtDomainRead(d, meta) + if err != nil { + return err + } + + // we must read devices again in order to set some missing ip/MAC/host mappings + for i := 0; i < netIfacesCount; i++ { + prefix := fmt.Sprintf("network_interface.%d", i) + + macI := d.Get(prefix + ".mac") + mac := strings.ToUpper(macI.(string)) + + // if we were waiting for an IP address for this MAC, go ahead. + if pending, ok := partialNetIfaces[mac]; ok { + // we should have the address now + if addressesI, ok := d.GetOk(prefix + ".addresses"); !ok { + return fmt.Errorf("Did not obtain the IP address for MAC=%s", mac) + } else { + for _, addressI := range addressesI.([]interface{}) { + address := addressI.(string) + log.Printf("[INFO] Finally adding IP/MAC/host=%s/%s/%s", address, mac, pending.hostname) + addHost(pending.network, address, mac, pending.hostname) + if err != nil { + return fmt.Errorf("Could not add IP/MAC/host=%s/%s/%s: %s", address, mac, pending.hostname, err) + } + } + } + } + } + + return nil } func resourceLibvirtDomainUpdate(d *schema.ResourceData, meta interface{}) error { @@ -207,7 +349,6 @@ func resourceLibvirtDomainUpdate(d *schema.ResourceData, meta interface{}) error if virConn == nil { return fmt.Errorf("The libvirt connection was nil.") } - domain, err := virConn.LookupByUUIDString(d.Id()) if err != nil { return fmt.Errorf("Error retrieving libvirt domain: %s", err) @@ -263,6 +404,42 @@ func resourceLibvirtDomainUpdate(d *schema.ResourceData, meta interface{}) error } } + netIfacesCount := d.Get("network_interface.#").(int) + for i := 0; i < netIfacesCount; i++ { + prefix := fmt.Sprintf("network_interface.%d", i) + if d.HasChange(prefix+".hostname") || d.HasChange(prefix+".addresses") || d.HasChange(prefix+".mac") { + networkUUID, ok := d.GetOk(prefix + ".network_id") + if !ok { + continue + } + network, err := virConn.LookupNetworkByUUIDString(networkUUID.(string)) + if err != nil { + return fmt.Errorf("Can't retrieve network ID %s", networkUUID) + } + defer network.Free() + + networkName, err := network.GetName() + if err != nil { + return fmt.Errorf("Error retrieving volume name: %s", err) + } + hostname := d.Get(prefix + ".hostname").(string) + mac := d.Get(prefix + ".mac").(string) + addresses := d.Get(prefix + ".addresses") + for _, addressI := range addresses.([]interface{}) { + address := addressI.(string) + ip := net.ParseIP(address) + if ip == nil { + return fmt.Errorf("Could not parse addresses '%s'", address) + } + log.Printf("[INFO] Updating IP/MAC/host=%s/%s/%s in '%s' network", ip.String(), mac, hostname, networkName) + if err := updateHost(&network, ip.String(), mac, hostname); err != nil { + return err + } + } + } + } + + // TODO return nil } @@ -316,7 +493,7 @@ func resourceLibvirtDomainRead(d *schema.ResourceData, meta interface{}) error { virVolKey, err := virVol.GetKey() if err != nil { - return fmt.Errorf("Error retrieving volume ke for disk: %s", err) + return fmt.Errorf("Error retrieving volume for disk: %s", err) } disk := map[string]interface{}{ @@ -345,51 +522,88 @@ func resourceLibvirtDomainRead(d *schema.ResourceData, meta interface{}) error { // we need it to read old values prefix := fmt.Sprintf("network_interface.%d", i) - if networkInterfaceDef.Type != "network" { - log.Printf("[DEBUG] ignoring interface of type '%s'", networkInterfaceDef.Type) - continue + netIface := map[string]interface{}{ + "network_id": "", + "network_name": "", + "bridge": "", + "vepa": "", + "macvtap": "", + "passthrough": "", + "mac": strings.ToUpper(networkInterfaceDef.Mac.Address), + "hostname": "", + "wait_for_lease": false, } - netIface := map[string]interface{}{ - "network": networkInterfaceDef.Source.Network, - "mac": networkInterfaceDef.Mac.Address, - } - - netIfaceAddrs := make([]map[string]interface{}, 0) - // look for an ip address and try to match it with the mac address - // not sure if using the target device name is a better idea here - for _, ifaceWithAddr := range ifacesWithAddr { - if ifaceWithAddr.Hwaddr == networkInterfaceDef.Mac.Address { - for _, addr := range ifaceWithAddr.Addrs { - netIfaceAddr := map[string]interface{}{ - "type": func() string { - switch addr.Type { - case libvirt.VIR_IP_ADDR_TYPE_IPV4: - return "ipv4" - case libvirt.VIR_IP_ADDR_TYPE_IPV6: - return "ipv6" - default: - return "other" + switch networkInterfaceDef.Type { + case "network": { + network, err := virConn.LookupNetworkByName(networkInterfaceDef.Source.Network) + if err != nil { + return fmt.Errorf("Can't retrieve network ID for '%s'", networkInterfaceDef.Source.Network) + } + defer network.Free() + + netIface["network_id"], err = network.GetUUIDString() + if err != nil { + return fmt.Errorf("Can't retrieve network ID for '%s'", networkInterfaceDef.Source.Network) + } + + networkDef, err := newDefNetworkfromLibvirt(&network) + if err != nil { + return err + } + + netIface["network_name"] = networkInterfaceDef.Source.Network + + // try to look for this MAC in the DHCP configuration for this VM + if networkDef.HasDHCP() { + hostnameSearch: + for _, ip := range networkDef.Ips { + if ip.Dhcp != nil { + for _, host := range ip.Dhcp.Hosts { + if strings.ToUpper(host.Mac) == netIface["mac"] { + log.Printf("[DEBUG] read: hostname for '%s': '%s'", netIface["mac"], host.Name) + netIface["hostname"] = host.Name + break hostnameSearch } - }(), - "address": addr.Addr, - "prefix": addr.Prefix, + } } - netIfaceAddrs = append(netIfaceAddrs, netIfaceAddr) } } - } - log.Printf("[DEBUG] %d addresses for %s\n", len(netIfaceAddrs), networkInterfaceDef.Mac.Address) - netIface["address"] = netIfaceAddrs + // look for an ip address and try to match it with the mac address + // not sure if using the target device name is a better idea here + addrs := make([]string, 0) + for _, ifaceWithAddr := range ifacesWithAddr { + if strings.ToUpper(ifaceWithAddr.Hwaddr) == netIface["mac"] { + for _, addr := range ifaceWithAddr.Addrs { + addrs = append(addrs, addr.Addr) + } + } + } + netIface["addresses"] = addrs + log.Printf("[DEBUG] read: addresses for '%s': %+v", netIface["mac"], addrs) + + netIface["wait_for_lease"] = d.Get(prefix + ".wait_for_lease").(bool) - // pass on old wait_for_lease value - if waitForLease, ok := d.GetOk(prefix + ".wait_for_lease"); ok { - netIface["wait_for_lease"] = waitForLease + } + case "bridge": + netIface["bridge"] = networkInterfaceDef.Source.Bridge + case "direct": + { + switch networkInterfaceDef.Source.Mode { + case "vepa": + netIface["vepa"] = networkInterfaceDef.Source.Dev + case "bridge": + netIface["macvtap"] = networkInterfaceDef.Source.Dev + case "passthrough": + netIface["passthrough"] = networkInterfaceDef.Source.Dev + } + } } netIfaces = append(netIfaces, netIface) } + log.Printf("[DEBUG] read: ifaces for '%s':\n%s", domainDef.Name, spew.Sdump(netIfaces)) d.Set("network_interface", netIfaces) if len(ifacesWithAddr) > 0 { @@ -407,6 +621,8 @@ func resourceLibvirtDomainDelete(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("The libvirt connection was nil.") } + log.Printf("[DEBUG] Deleting domain %s", d.Id()) + domain, err := virConn.LookupByUUIDString(d.Id()) if err != nil { return fmt.Errorf("Error retrieving libvirt domain: %s", err) @@ -463,7 +679,8 @@ func waitForNetworkAddresses(ifaces []defNetworkInterface, domain libvirt.VirDom continue } - if iface.Mac.Address == "" { + mac := strings.ToUpper(iface.Mac.Address) + if mac == "" { log.Printf("[DEBUG] Can't wait without a mac address.\n") // we can't get the ip without a mac address continue @@ -471,6 +688,7 @@ func waitForNetworkAddresses(ifaces []defNetworkInterface, domain libvirt.VirDom // loop until address appear, with timeout start := time.Now() + waitLoop: for { log.Printf("[DEBUG] waiting for network address for interface with hwaddr: '%s'\n", iface.Mac.Address) @@ -478,10 +696,11 @@ func waitForNetworkAddresses(ifaces []defNetworkInterface, domain libvirt.VirDom if err != nil { return fmt.Errorf("Error retrieving interface addresses: %s", err) } + log.Printf("[DEBUG] ifaces with addresses: %+v\n", ifacesWithAddr) for _, ifaceWithAddr := range ifacesWithAddr { // found - if iface.Mac.Address == ifaceWithAddr.Hwaddr { + if mac == strings.ToUpper(ifaceWithAddr.Hwaddr) { break waitLoop } } diff --git a/libvirt/resource_libvirt_domain_netiface.go b/libvirt/resource_libvirt_domain_netiface.go new file mode 100644 index 00000000..88c5e69b --- /dev/null +++ b/libvirt/resource_libvirt_domain_netiface.go @@ -0,0 +1,68 @@ +package libvirt + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +func networkInterfaceCommonSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "network_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "network_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "bridge": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "vepa": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "macvtap": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "passthrough": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "hostname": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: false, + }, + "mac": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "wait_for_lease": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "addresses": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + ForceNew: false, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + } +} diff --git a/libvirt/resource_libvirt_domain_test.go b/libvirt/resource_libvirt_domain_test.go index be778355..cabbad0b 100644 --- a/libvirt/resource_libvirt_domain_test.go +++ b/libvirt/resource_libvirt_domain_test.go @@ -2,12 +2,13 @@ package libvirt import ( "fmt" + "log" + "testing" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" //"gopkg.in/alexzorin/libvirt-go.v2" libvirt "github.com/dmacvicar/libvirt-go" - "log" - "testing" ) func TestAccLibvirtDomain_Basic(t *testing.T) { diff --git a/libvirt/resource_libvirt_network.go b/libvirt/resource_libvirt_network.go new file mode 100644 index 00000000..063a274c --- /dev/null +++ b/libvirt/resource_libvirt_network.go @@ -0,0 +1,331 @@ +package libvirt + +import ( + "fmt" + "log" + "net" + "strings" + "time" + + libvirt "github.com/dmacvicar/libvirt-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + netModeIsolated = "none" + netModeNat = "nat" + netModeRoute = "route" + netModeBridge = "bridge" +) + +// a libvirt network resource +// +// Resource example: +// +// resource "libvirt_network" "k8snet" { +// name = "k8snet" +// domain = "k8s.local" +// mode = "nat" +// addresses = ["10.17.3.0/24"] +// } +// +// "addresses" can contain (0 or 1) ipv4 and (0 or 1) ipv6 ranges +// "mode" can be one of: "nat" (default), "isolated" +// +func resourceLibvirtNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceLibvirtNetworkCreate, + Read: resourceLibvirtNetworkRead, + Delete: resourceLibvirtNetworkDelete, + Exists: resourceLibvirtNetworkExists, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "domain": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "mode": &schema.Schema{ // can be "none", "nat" (default), "route", "bridge" + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: netModeNat, + }, + "bridge": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "addresses": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Required: false, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceLibvirtNetworkExists(d *schema.ResourceData, meta interface{}) (bool, error) { + virConn := meta.(*Client).libvirt + if virConn == nil { + return false, fmt.Errorf("The libvirt connection was nil.") + } + network, err := virConn.LookupNetworkByUUIDString(d.Id()) + defer network.Free() + return err == nil, err +} + +func resourceLibvirtNetworkCreate(d *schema.ResourceData, meta interface{}) error { + // see https://libvirt.org/formatnetwork.html + virConn := meta.(*Client).libvirt + if virConn == nil { + return fmt.Errorf("The libvirt connection was nil.") + } + + networkDef := newNetworkDef() + networkDef.Name = d.Get("name").(string) + networkDef.Domain = &defNetworkDomain{ + Name: d.Get("domain").(string), + } + + // use a bridge provided by the user, or create one otherwise (libvirt will assign on automatically when empty) + bridgeName := "" + if b, ok := d.GetOk("bridge"); ok { + bridgeName = b.(string) + } + networkDef.Bridge = &defNetworkBridge{ + Name: bridgeName, + Stp: "on", + } + + // check the network mode + networkDef.Forward.Mode = strings.ToLower(d.Get("mode").(string)) + if networkDef.Forward.Mode == netModeIsolated || networkDef.Forward.Mode == netModeNat || networkDef.Forward.Mode == netModeRoute { + + // there is no mode when using an isolated network + if networkDef.Forward.Mode == netModeIsolated { + networkDef.Forward = nil + } + + // some network modes require a DHCP/DNS server + // set the addresses for DHCP + if addresses, ok := d.GetOk("addresses"); ok { + ipsPtrsLst := []*defNetworkIp{} + for _, addressI := range addresses.([]interface{}) { + address := addressI.(string) + _, ipNet, err := net.ParseCIDR(address) + if err != nil { + return fmt.Errorf("Error parsing addresses definition '%s': %s", address, err) + } + ones, bits := ipNet.Mask.Size() + family := "ipv4" + if bits == (net.IPv6len * 8) { + family = "ipv6" + } + ipsRange := 2 ^ bits - 2 ^ ones + if ipsRange < 4 { + return fmt.Errorf("Netmask seems to be too strict: only %d IPs available (%s)", ipsRange-3, family) + } + + // we should calculate the range served by DHCP. For example, for + // 192.168.121.0/24 we will serve 192.168.121.2 - 192.168.121.254 + start, end := NetworkRange(ipNet) + + // skip the .0, (for the network), + start[len(start)-1]++ + + // assign the .1 to the host interface + dni := defNetworkIp{ + Address: start.String(), + Prefix: ones, + Family: family, + } + + start[len(start)-1]++ // then skip the .1 + end[len(end)-1]-- // and skip the .255 (for broadcast) + + dni.Dhcp = &defNetworkIpDhcp{ + Ranges: []*defNetworkIpDhcpRange{ + &defNetworkIpDhcpRange{ + Start: start.String(), + End: end.String(), + }, + }, + } + ipsPtrsLst = append(ipsPtrsLst, &dni) + } + networkDef.Ips = ipsPtrsLst + } + } else if networkDef.Forward.Mode == netModeBridge { + if bridgeName == "" { + return fmt.Errorf("'bridge' must be provided when using the bridged network mode") + } + } else { + return fmt.Errorf("unsuppoorted network mode '%s'", networkDef.Forward.Mode) + } + + // once we have the network defined, connect to libvirt and create it from the XML serialization + connectURI, err := virConn.GetURI() + if err != nil { + return fmt.Errorf("Error retrieving libvirt connection URI: %s", err) + } + log.Printf("[INFO] Creating libvirt network at %s", connectURI) + + data, err := xmlMarshallIndented(networkDef) + if err != nil { + return fmt.Errorf("Error serializing libvirt network: %s", err) + } + + log.Printf("[DEBUG] Creating libvirt network at %s: %s", connectURI, data) + network, err := virConn.NetworkDefineXML(data) + if err != nil { + return fmt.Errorf("Error defining libvirt network: %s - %s", err, data) + } + err = network.Create() + if err != nil { + return fmt.Errorf("Error crearing libvirt network: %s", err) + } + defer network.Free() + + id, err := network.GetUUIDString() + if err != nil { + return fmt.Errorf("Error retrieving libvirt network id: %s", err) + } + d.SetId(id) + + log.Printf("[INFO] Created network %s [%s]", networkDef.Name, d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"BUILD"}, + Target: []string{"ACTIVE"}, + Refresh: waitForNetworkActive(network), + Timeout: 1 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for network to reach ACTIVE state: %s", err) + } + + return resourceLibvirtNetworkRead(d, meta) +} + +func resourceLibvirtNetworkRead(d *schema.ResourceData, meta interface{}) error { + virConn := meta.(*Client).libvirt + if virConn == nil { + return fmt.Errorf("The libvirt connection was nil.") + } + + network, err := virConn.LookupNetworkByUUIDString(d.Id()) + if err != nil { + return fmt.Errorf("Error retrieving libvirt network: %s", err) + } + defer network.Free() + + networkDef, err := newDefNetworkfromLibvirt(&network) + if err != nil { + return fmt.Errorf("Error reading libvirt network XML description: %s", err) + } + + d.Set("name", networkDef.Name) + d.Set("domain", networkDef.Domain.Name) + d.Set("bridge", networkDef.Bridge.Name) + + addresses := []string{} + for _, address := range networkDef.Ips { + // we get the host interface IP (ie, 10.10.8.1) but we want the network CIDR (ie, 10.10.8.0/24) + // so we need some transformations... + addr := net.ParseIP(address.Address) + if addr == nil { + return fmt.Errorf("Error parsing IP '%s': %s", address, err) + } + bits := net.IPv6len * 8 + if addr.To4() != nil { + bits = net.IPv4len * 8 + } + mask := net.CIDRMask(address.Prefix, bits) + network := addr.Mask(mask) + addresses = append(addresses, fmt.Sprintf("%s/%d", network, address.Prefix)) + } + if len(addresses) > 0 { + d.Set("addresses", addresses) + } + + // TODO: get any other parameters from the network and save them + + log.Printf("[DEBUG] Network ID %s successfully read", d.Id()) + return nil +} + +func resourceLibvirtNetworkDelete(d *schema.ResourceData, meta interface{}) error { + virConn := meta.(*Client).libvirt + if virConn == nil { + return fmt.Errorf("The libvirt connection was nil.") + } + log.Printf("[DEBUG] Deleting network ID %s", d.Id()) + + network, err := virConn.LookupNetworkByUUIDString(d.Id()) + if err != nil { + return fmt.Errorf("When destroying libvirt network: error retrieving %s", err) + } + defer network.Free() + + if err := network.Destroy(); err != nil { + return fmt.Errorf("When destroying libvirt network: %s", err) + } + + if err := network.Destroy(); err != nil { + return fmt.Errorf("When destroying libvirt network: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"ACTIVE"}, + Target: []string{"NOT-EXISTS"}, + Refresh: waitForNetworkDestroyed(virConn, d.Id()), + Timeout: 1 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for network to reach NOT-EXISTS state: %s", err) + } + return nil +} + +func waitForNetworkActive(network libvirt.VirNetwork) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + active, err := network.IsActive() + if err != nil { + return nil, "", err + } + if active { + return network, "ACTIVE", nil + } + return network, "BUILD", err + } +} + +// wait for network to be up and timeout after 5 minutes. +func waitForNetworkDestroyed(virConn *libvirt.VirConnection, uuid string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("Waiting for network %s to be destroyed", uuid) + network, err := virConn.LookupNetworkByUUIDString(uuid) + if err.(libvirt.VirError).Code == libvirt.VIR_ERR_NO_NETWORK { + return virConn, "NOT-EXISTS", nil + } + defer network.Free() + return virConn, "ACTIVE", err + } +} diff --git a/libvirt/utils.go b/libvirt/utils.go index 1fc412c8..da1502f7 100644 --- a/libvirt/utils.go +++ b/libvirt/utils.go @@ -1,11 +1,14 @@ package libvirt import ( - "crypto/rand" + "bytes" + "encoding/xml" "fmt" - libvirt "github.com/dmacvicar/libvirt-go" "log" "time" + + libvirt "github.com/dmacvicar/libvirt-go" + "github.com/davecgh/go-spew/spew" ) var diskLetters []rune = []rune("abcdefghijklmnopqrstuvwxyz") @@ -40,24 +43,15 @@ func WaitForSuccess(errorMessage string, f func() error) error { } } -func RandomMACAddress() (string, error) { - buf := make([]byte, 6) - _, err := rand.Read(buf) - if err != nil { - return "", err - } - - // set local bit and unicast - buf[0] = (buf[0] | 2) & 0xfe - // Set the local bit - buf[0] |= 2 - - // avoid libvirt-reserved addresses - if buf[0] == 0xfe { - buf[0] = 0xee +// return an indented XML +func xmlMarshallIndented(b interface{}) (string, error) { + buf := new(bytes.Buffer) + enc := xml.NewEncoder(buf) + enc.Indent(" ", " ") + if err := enc.Encode(b); err != nil { + fmt.Errorf("could not marshall this:\n%s", spew.Sdump(b)) } - - return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]), nil + return buf.String(), nil } // Remove the volume identified by `key` from libvirt diff --git a/libvirt/utils_libvirt.go b/libvirt/utils_libvirt.go new file mode 100644 index 00000000..e14e9554 --- /dev/null +++ b/libvirt/utils_libvirt.go @@ -0,0 +1,41 @@ +package libvirt + +import ( + "log" + + libvirt "github.com/dmacvicar/libvirt-go" +) + +func getHostXMLDesc(ip, mac, name string) string { + dd := defNetworkIpDhcpHost{ + Ip: ip, + Mac: mac, + Name: name, + } + xml, err := xmlMarshallIndented(dd) + if err != nil { + panic("could not marshall host") + } + return xml +} + +// Adds a new static host to the network +func addHost(n *libvirt.VirNetwork, ip, mac, name string) error { + xmlDesc := getHostXMLDesc(ip, mac, name) + log.Printf("Adding host with XML:\n%s", xmlDesc) + return n.UpdateXMLDesc(xmlDesc, libvirt.VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST) +} + +// Removes a static host from the network +func removeHost(n *libvirt.VirNetwork, ip, mac, name string) error { + xmlDesc := getHostXMLDesc(ip, mac, name) + log.Printf("Removing host with XML:\n%s", xmlDesc) + return n.UpdateXMLDesc(xmlDesc, libvirt.VIR_NETWORK_UPDATE_COMMAND_DELETE, libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST) +} + +// Update a static host from the network +func updateHost(n *libvirt.VirNetwork, ip, mac, name string) error { + xmlDesc := getHostXMLDesc(ip, mac, name) + log.Printf("Updating host with XML:\n%s", xmlDesc) + return n.UpdateXMLDesc(xmlDesc, libvirt.VIR_NETWORK_UPDATE_COMMAND_MODIFY, libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST) +} diff --git a/libvirt/utils_net.go b/libvirt/utils_net.go new file mode 100644 index 00000000..8f96d6a3 --- /dev/null +++ b/libvirt/utils_net.go @@ -0,0 +1,58 @@ +package libvirt + +import ( + "crypto/rand" + "fmt" + "net" +) + +const ( + maxIfaceNum = 100 +) + +func RandomMACAddress() (string, error) { + buf := make([]byte, 6) + _, err := rand.Read(buf) + if err != nil { + return "", err + } + + // set local bit and unicast + buf[0] = (buf[0] | 2) & 0xfe + // Set the local bit + buf[0] |= 2 + + // avoid libvirt-reserved addresses + if buf[0] == 0xfe { + buf[0] = 0xee + } + + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]), nil +} + +func FreeNetworkInterface(basename string) (string, error) { + for i := 0; i < maxIfaceNum; i++ { + ifaceName := fmt.Sprintf("%s%d", basename, i) + _, err := net.InterfaceByName(ifaceName) + if err != nil { + return ifaceName, nil + } + } + return "", fmt.Errorf("could not obtain a free network interface") +} + +// Calculates the first and last IP addresses in an IPNet +func NetworkRange(network *net.IPNet) (net.IP, net.IP) { + netIP := network.IP.To4() + lastIP := net.IPv4(0, 0, 0, 0).To4() + if netIP == nil { + netIP = network.IP.To16() + lastIP = net.IPv6zero.To16() + } + firstIP := netIP.Mask(network.Mask) + for i := 0; i < len(lastIP); i++ { + lastIP[i] = netIP[i] | ^network.Mask[i] + } + return firstIP, lastIP +} diff --git a/libvirt/utils_test.go b/libvirt/utils_test.go index 671964cf..62000f34 100644 --- a/libvirt/utils_test.go +++ b/libvirt/utils_test.go @@ -1,6 +1,7 @@ package libvirt import ( + "net" "testing" ) @@ -16,3 +17,18 @@ func TestDiskLetterForIndex(t *testing.T) { } } } + +func TestIPsRange(t *testing.T) { + _, net, err := net.ParseCIDR("192.168.18.1/24") + if err != nil { + t.Errorf("When parsing network: %s", err) + } + + start, end := NetworkRange(net) + if start.String() != "192.168.18.0" { + t.Errorf("unexpected range start for '%s': %s", net, start) + } + if end.String() != "192.168.18.255" { + t.Errorf("unexpected range start for '%s': %s", net, start) + } +} |