[CORD-2270]
Support head node on Ubuntu 16.04 (Xenial)

Change-Id: Ic13ea784b8fa55a481f08d21f5187fd37d13499c
diff --git a/copy-profile-playbook.yml b/copy-profile-playbook.yml
index 9530fe0..a093527 100644
--- a/copy-profile-playbook.yml
+++ b/copy-profile-playbook.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
----
 # copy-profile-playbook.yml
 # Copies the profile to the head node
 
@@ -32,7 +30,7 @@
   roles:
     - { role: copy-profile, become: yes }
     - { role: ssh-install, become: yes }
-    - { role: glance-images, become: yes, when: use_maas }
+    - { role: glance-images, become: yes, when: ( use_openstack | default(True) ) }
     - { role: copy-credentials, become: yes, when: not ( frontend_only | default(False) ) }
 
 - name: Install ssh keys when using MaaS
diff --git a/filter_plugins/genmac.py b/filter_plugins/genmac.py
deleted file mode 100644
index 02142ae..0000000
--- a/filter_plugins/genmac.py
+++ /dev/null
@@ -1,45 +0,0 @@
-
-# Copyright 2017-present Open Networking Foundation
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-import hashlib
-import netaddr
-
-def genmac(value, prefix='', length=12):
-    '''
-    deterministically generates a "random" MAC with a configurable prefix
-    '''
-
-    # from: http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines
-    if prefix == '' :
-        mac_prefix = "0ac04d" # random "cord"-esque
-
-    # deterministically generate a value
-    h = hashlib.new('sha1')
-    h.update(value)
-
-    # build/trim MAC
-    mac_string = (mac_prefix + h.hexdigest())[0:length]
-
-    return netaddr.EUI(mac_string)
-
-class FilterModule(object):
-    ''' MAC generation filter '''
-    filter_map = {
-        'genmac': genmac,
-    }
-
-    def filters(self):
-         return self.filter_map
diff --git a/filter_plugins/unbound_revdns.py b/filter_plugins/unbound_revdns.py
new file mode 100644
index 0000000..a45e08e
--- /dev/null
+++ b/filter_plugins/unbound_revdns.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# unbound_revdns - ansible filter - ipv4 address or cidr and generates a
+# reverse DNS string suitable for use with unbound, which requires them in a
+# slightly unusual format when a RFC1918 address is used with a local-zone
+# definition:
+# https://www.unbound.net/documentation/unbound.conf.html
+# https://www.claudiokuenzler.com/blog/699/unbound-dns-not-serving-reverse-lookup-for-internal-addresses-rfc1918
+
+import netaddr
+
+
+class FilterModule(object):
+
+    def filters(self):
+        return {
+            'unbound_revdns': self.unbound_revdns,
+            }
+
+    def unbound_revdns(self, var):
+        (o1, o2, o3, o4) = netaddr.IPNetwork(var).network.words
+
+        revdns = "%d.%d.%d.%d.in-addr.arpa." % (o4, o3, o2, o1)
+
+        if (o2 == 0 or o1 == 10):
+            revdns = "%d.in-addr.arpa." % (o1)
+        elif (o3 == 0
+                or (o1 == 172 and o2 >= 16 and o2 <= 31)
+                or (o1 == 192 and o2 == 168)):
+            revdns = "%d.%d.in-addr.arpa." % (o2, o1)
+        elif(o4 == 0):
+            revdns = "%d.%d.%d.in-addr.arpa." % (o3, o2, o1)
+
+        return revdns
diff --git a/prep-headnode-playbook.yml b/prep-headnode-playbook.yml
index d9d64b3..9d89e50 100644
--- a/prep-headnode-playbook.yml
+++ b/prep-headnode-playbook.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
----
 # prep-headnode-playbook.yml
 # Preps the head node of a CORD pod for thr est of the install
 
@@ -44,7 +42,7 @@
   roles:
     - docker-install
 
-- name: Configure management network
+- name: Configure network interfaces
   hosts: head
   become: yes
   roles:
@@ -54,7 +52,6 @@
   hosts: head
   become: yes
   roles:
-    - { role: head-mgmtbr, when: not use_maas }
     - { role: dns-nsd, when: not use_maas }
     - { role: dns-unbound, when: not use_maas }
 
diff --git a/roles/dhcpd/defaults/main.yml b/roles/dhcpd/defaults/main.yml
index 66edc52..9772910 100644
--- a/roles/dhcpd/defaults/main.yml
+++ b/roles/dhcpd/defaults/main.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,18 +13,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
----
 # dhcpd/defaults/main.yml
 
-# http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines
-hwaddr_prefix: "c2a4"
-
 site_name: placeholder-sitename
 site_suffix: "{{ site_name }}.test"
 
-# Management IP range from DHCP settings
-mgmt_ipv4_first_octets: "192.168.200"
+management_net_cidr: "192.168.200.0/24"
+
+vtn_net_management_host_cidr: "{{ management_net_cidr }}"
+
+# http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines
+vtn_net_management_host_hwaddr_prefix: "06A6"
 
 # node lists
 head_lxd_list: []
@@ -33,13 +32,10 @@
 dns_search:
   - "{{ site_suffix }}"
 
-dns_servers:
-  - "{{ mgmt_ipv4_first_octets }}.1"
-
 dhcpd_subnets:
-  - interface: mgmtbr
-    cidr: "{{ mgmt_ipv4_first_octets }}.1/24"
-    dhcp_first: 129
+  - interface: mgmtbridge
+    cidr: "{{ management_net_cidr }}"
+    dhcp_first: 193
     dhcp_last: 254
     other_static:
       - physical_node_list
diff --git a/roles/dhcpd/templates/dhcpd.conf.j2 b/roles/dhcpd/templates/dhcpd.conf.j2
index 072d6d7..c54e578 100644
--- a/roles/dhcpd/templates/dhcpd.conf.j2
+++ b/roles/dhcpd/templates/dhcpd.conf.j2
@@ -1,4 +1,3 @@
-
 {#
 Copyright 2017-present Open Networking Foundation
 
@@ -15,7 +14,6 @@
 limitations under the License.
 #}
 
-
 # dhcpd.conf
 # Managed by Ansible!
 
@@ -52,7 +50,7 @@
     option host-name "{{ node.name }}";
 {% set host_ipaddr = (subnet.cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address')) %}
     fixed-address {{ host_ipaddr }};
-    hardware ethernet {{ node.hwaddr | default(hwaddr_prefix ~ (host_ipaddr | ip4_hex)) | hwaddr('unix') }};
+    hardware ethernet {{ node.hwaddr | default(vtn_net_management_host_hwaddr_prefix ~ (host_ipaddr | ip4_hex)) | hwaddr('unix') }};
 {% if node.pxe_filename is defined %}
     filename "{{ node.pxe_filename }}";
     next-server {{ subnet.tftp_server | default(subnet.cidr | ipaddr('1') | ipaddr('address')) }};
@@ -69,7 +67,7 @@
     option host-name "{{ node.name }}";
 {% set host_ipaddr = (subnet.cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address')) %}
     fixed-address {{ host_ipaddr }};
-    hardware ethernet {{ node.hwaddr | default(hwaddr_prefix ~ (host_ipaddr | ip4_hex)) | hwaddr('unix') }};
+    hardware ethernet {{ node.hwaddr | default(vtn_net_management_host_hwaddr_prefix ~ (host_ipaddr | ip4_hex)) | hwaddr('unix') }};
 {% if node.pxe_filename is defined %}
     filename "{{ subnet.pxe_filename }}";
     next-server {{ subnet.tftp_server | default(subnet.cidr | ipaddr('1') | ipaddr('address')) }};
diff --git a/roles/dns-configure/defaults/main.yml b/roles/dns-configure/defaults/main.yml
index 777b1e2..19cc84c 100644
--- a/roles/dns-configure/defaults/main.yml
+++ b/roles/dns-configure/defaults/main.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,17 +13,58 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
----
 # roles/dns-configure/defaults
 
+site_name: placeholder-sitename
+site_suffix: "{{ site_name }}.test"
+
+headnode_dns: head1
+
+management_net_cidr: "192.168.200.0/24"
+
+# node lists
+head_lxd_list: []
+physical_node_list: []
+
+# Set this to search domain suffixes
+dns_search:
+  - "{{ site_suffix }}"
+
+dns_servers:
+  - "{{ management_net_cidr | ipaddr('1') | ipaddr('address') }}"
+
+unbound_listen_on_default: False
+
 # Define this to set dns servers manually
 #dns_servers:
 #  - 8.8.8.8
 #  - 8.8.4.4
 
-# Set this to search domain suffixes
-# dns_search: {}
-
-unbound_listen_on_default: False
+# DNS settings for NSD/Unbound
+nsd_zones:
+  - name: "{{ site_suffix }}"
+    cidr: "{{ management_net_cidr }}"
+    soa: ns1
+    ns:
+      - { name: ns1 }
+    nodelists:
+      - head_lxd_list
+      - physical_node_list
+    aliases:
+      - { name: "apt-cache", dest: "{{ headnode_dns }}" }
+      - { name: "cordloghost", dest: "{{ headnode_dns }}" }
+      - { name: "consul", dest: "{{ headnode_dns }}" }
+      - { name: "docker", dest: "{{ headnode_dns }}" }
+      - { name: "mavenrepo", dest: "{{ headnode_dns }}" }
+      - { name: "ns", dest: "{{ headnode_dns }}" }
+      - { name: "ns1", dest: "{{ headnode_dns }}" }
+      - { name: "onos-cord", dest: "{{ headnode_dns }}" }
+      - { name: "onos-fabric", dest: "{{ headnode_dns }}" }
+      - { name: "xos", dest: "{{ headnode_dns }}" }
+      - { name: "xos-chameleon", dest: "{{ headnode_dns }}" }
+      - { name: "xos-consul", dest: "{{ headnode_dns }}" }
+      - { name: "xos-core", dest: "{{ headnode_dns }}" }
+      - { name: "xos-gui", dest: "{{ headnode_dns }}" }
+      - { name: "xos-tosca", dest: "{{ headnode_dns }}" }
+      - { name: "xos-ws", dest: "{{ headnode_dns }}" }
 
diff --git a/roles/dns-configure/tasks/main.yml b/roles/dns-configure/tasks/main.yml
index 83c3a6f..5ea0418 100644
--- a/roles/dns-configure/tasks/main.yml
+++ b/roles/dns-configure/tasks/main.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
----
 # roles/dns-configure/tasks.yml
 
 - name: Make sure resolvconf is doing DNS resolver mangling
@@ -48,3 +46,12 @@
   tags:
    - skip_ansible_lint # purely a way to pass/fail config done so far. Ansible needs a "dns_query" module
 
+- name: Check that aliases can be found in DNS
+  when: not use_maas
+  shell: "dig +short {{ item.1.name }}.{{ site_suffix }} | grep {{ item.1.dest }}"
+  with_subelements:
+    - "{{ nsd_zones }}"
+    - aliases
+  tags:
+   - skip_ansible_lint # purely a way to pass/fail config done so far. Ansible needs a "dns_query" module
+
diff --git a/roles/dns-nsd/defaults/main.yml b/roles/dns-nsd/defaults/main.yml
index 4e80a0c..20f0fde 100644
--- a/roles/dns-nsd/defaults/main.yml
+++ b/roles/dns-nsd/defaults/main.yml
@@ -15,39 +15,34 @@
 
 # dns-nsd/defaults/main.yml
 
-nsd_ip: 127.0.0.1
-
-nsd_conf: "/etc/nsd/nsd.conf"
-nsd_zonesdir: "/var/lib/nsd/zones"
-nsd_group: "nsd"
-
-# default DNS TTL
-dns_ttl: 3600
-
-# NOTE - many of the below settings are shared with the dns-nsd role, and you
-# may need to update them in the defaults of both.
-
-headnode_dns: head1
-
 site_name: placeholder-sitename
 site_suffix: "{{ site_name }}.test"
 
-# Management IP range from DHCP settings
-mgmt_ipv4_first_octets: "192.168.200"
-mgmt_name_reverse_unbound: "168.192.in-addr.arpa"
+headnode_dns: head1
 
-dns_servers:
-  - "{{ mgmt_ipv4_first_octets }}.1"
+management_net_cidr: "192.168.200.0/24"
 
 # node lists
 head_lxd_list: []
 physical_node_list: []
 
+# NOTE - many of the below settings are shared with the dns-nsd role, and you
+# may need to update them in the defaults of both.
+
+nsd_conf: "/etc/nsd/nsd.conf"
+nsd_zonesdir: "/var/lib/nsd/zones"
+
+nsd_group: "nsd"
+
+nsd_ip: 127.0.0.1
+nsd_port: 53
+
+dns_ttl: 3600
+
 # DNS settings for NSD/Unbound
 nsd_zones:
   - name: "{{ site_suffix }}"
-    ipv4_first_octets: "{{ mgmt_ipv4_first_octets }}"
-    name_reverse_unbound: "{{ mgmt_name_reverse_unbound }}"
+    cidr: "{{ management_net_cidr }}"
     soa: ns1
     ns:
       - { name: ns1 }
diff --git a/roles/dns-nsd/tasks/main.yml b/roles/dns-nsd/tasks/main.yml
index 91eeabd..83e35ab 100644
--- a/roles/dns-nsd/tasks/main.yml
+++ b/roles/dns-nsd/tasks/main.yml
@@ -13,7 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
 # dns-nsd/tasks/main.yml
 
 - name: Install nsd
@@ -24,8 +23,18 @@
     cache_valid_time: 3600
   with_items:
     - nsd
+  register: nsd_install
 
-- name: Ensure that zones directory exists
+- name: Stop nsd until configured
+  when: nsd_install.changed
+  service:
+    name: nsd
+    enabled: no
+    state: stopped
+  tags:
+    - skip_ansible_lint # need to down service before configured
+
+- name: Create nsd zones directory
   file:
     name: "{{ nsd_zonesdir }}"
     state: directory
@@ -43,7 +52,7 @@
   notify:
     - restart-nsd
 
-- name: create forward zonefiles from template
+- name: Create forward zonefiles from template
   template:
     src: zone.forward.j2
     dest: "{{ nsd_zonesdir }}/{{ item.name }}.forward"
@@ -54,7 +63,7 @@
   notify:
     - reload-nsd
 
-- name: create reverse zonefiles from template
+- name: Create reverse zonefiles from template
   template:
     src: zone.reverse.j2
     dest: "{{ nsd_zonesdir }}/{{ item.name }}.reverse"
diff --git a/roles/dns-nsd/templates/nsd.conf.j2 b/roles/dns-nsd/templates/nsd.conf.j2
index 4d5ead7..8e6c185 100644
--- a/roles/dns-nsd/templates/nsd.conf.j2
+++ b/roles/dns-nsd/templates/nsd.conf.j2
@@ -1,4 +1,3 @@
-
 {#
 Copyright 2017-present Open Networking Foundation
 
@@ -15,31 +14,30 @@
 limitations under the License.
 #}
 
-
 # nsd.conf
-# configured by Ansible!
+# created by dns-nsd/templates/nsd.conf.j2
 
 server:
   hide-version: yes
 ## bind to a specific address/port
   ip-address: {{ nsd_ip }}
 ## port number
-  port: {{ nsd_port|default(53) }} 
+  port: {{ nsd_port }}
   server-count: 1
   ip4-only: yes
   zonesdir: {{ nsd_zonesdir }}
 
 remote-control:
-  control-enable: no
+  control-enable: yes
 
-# zones to load
+# zonefiles to load
 {% for zone in nsd_zones %}
 zone:
   name: {{ zone.name }}
   zonefile: {{ zone.name }}.forward
 
 zone:
-  name: {{ (zone.ipv4_first_octets ~ ".0") | ipaddr('revdns') | regex_replace('^0\.','') }} 
+  name: {{ zone.cidr | unbound_revdns }}
   zonefile: {{ zone.name }}.reverse
 
 {% endfor %}
diff --git a/roles/dns-nsd/templates/zone.forward.j2 b/roles/dns-nsd/templates/zone.forward.j2
index 613576f..86eae0c 100644
--- a/roles/dns-nsd/templates/zone.forward.j2
+++ b/roles/dns-nsd/templates/zone.forward.j2
@@ -1,4 +1,3 @@
-
 {#
 Copyright 2017-present Open Networking Foundation
 
@@ -15,10 +14,9 @@
 limitations under the License.
 #}
 
-
 ;## NSD authoritative only DNS
 ;## FORWARD Zone
-;# created by ansible
+;# created by dns-nsd/templates/zone.forward.j2
 
 $ORIGIN {{ item.name }}. ; default zone domain
 $TTL {{ item.ttl | default(dns_ttl) }} ; default time to live
@@ -37,9 +35,6 @@
 {% endfor %}
 
 ;A and CNAME records
-{% if name_on_public_interface is defined %}
-{{ name_on_public_interface }}    IN    A    {{ ansible_default_ipv4.address }}
-{% endif %}
 {% if item.aliases is defined %}
 {% for alias in item.aliases %}
 {{ alias.name }}    IN    CNAME    {{ alias.dest }}
@@ -50,7 +45,7 @@
 ; Created from nodelist: {{ nodelist }}
 {% set nodes = vars[nodelist] %}
 {% for node in nodes %}
-{{ node.name }}    IN    A    {{ item.ipv4_first_octets ~ "." ~ node.ipv4_last_octet }}
+{{ node.name }}    IN    A    {{ item.cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address') }}
 {% if node.aliases is defined %}
 {% for alias in node.aliases %}
 {{ alias }}    IN    CNAME    {{ node.name }}
diff --git a/roles/dns-nsd/templates/zone.reverse.j2 b/roles/dns-nsd/templates/zone.reverse.j2
index 1ddf7ba..fdfd73f 100644
--- a/roles/dns-nsd/templates/zone.reverse.j2
+++ b/roles/dns-nsd/templates/zone.reverse.j2
@@ -1,4 +1,3 @@
-
 {#
 Copyright 2017-present Open Networking Foundation
 
@@ -15,7 +14,6 @@
 limitations under the License.
 #}
 
-
 ;## NSD authoritative only DNS
 ;## REVERSE Zone for {{ item.name }}
 ;# created by ansible
@@ -23,7 +21,7 @@
 $ORIGIN {{ item.name }}. ; default zone domain
 $TTL {{ item.ttl | default(dns_ttl) }} ; default time to live
 
-{{ (item.ipv4_first_octets ~ ".0") | ipaddr('revdns') | regex_replace('^0\.','') }} IN SOA {{ item.soa }}.{{ item.name }}. admin.{{ item.name }}. (
+{{ item.cidr | unbound_revdns }} IN SOA {{ item.soa }}.{{ item.name }}. admin.{{ item.name }}. (
          {{ item.serial | default(ansible_date_time.epoch) }}   ; Serial, must be incremented every time you change this file
          3600        ; Refresh [1hr]
          600         ; Retry [10m]
@@ -37,7 +35,7 @@
 ; Created from nodelist: {{ nodelist }}
 {% set nodes = vars[nodelist] %}
 {% for node in nodes %}
-{{ (item.ipv4_first_octets ~ "." ~ node.ipv4_last_octet) | ipaddr('revdns') }} IN PTR {{ node.name }}
+{{ item.cidr | ipaddr(node.ipv4_last_octet) | ipaddr('revdns') }} IN PTR {{ node.name }}
 {% endfor %}
 {% endfor %}
 
diff --git a/roles/dns-unbound/defaults/main.yml b/roles/dns-unbound/defaults/main.yml
index a3f4aa7..6dec81b 100644
--- a/roles/dns-unbound/defaults/main.yml
+++ b/roles/dns-unbound/defaults/main.yml
@@ -15,37 +15,34 @@
 
 # dns-unbound/defaults/main.yml
 
-unbound_conf: "/etc/unbound/unbound.conf"
-unbound_group: "unbound"
-
-unbound_listen_on_default: False
-
-unbound_listen_all: True
-
-# NOTE - many of the below settings are shared with the dns-nsd role, and you
-# may need to update them in the defaults of both.
-
+# Shared settings
 site_name: placeholder-sitename
 site_suffix: "{{ site_name }}.test"
 
 headnode_dns: head1
 
-# Management IP range from DHCP settings
-mgmt_ipv4_first_octets: "192.168.200"
-mgmt_name_reverse_unbound: "168.192.in-addr.arpa"
-
-unbound_interfaces:
-  - "{{ mgmt_ipv4_first_octets }}.1/24"
+management_net_cidr: "192.168.200.0/24"
 
 # node lists
 head_lxd_list: []
 physical_node_list: []
 
+# NOTE - many of the below settings are shared with the dns-nsd role, and you
+# may need to update them in the defaults of both.
+
+nsd_ip: 127.0.0.1
+
+unbound_conf: "/etc/unbound/unbound.conf"
+unbound_group: "unbound"
+
+unbound_listen_on_default: False
+unbound_listen_all: True
+unbound_listen_zones: True
+
 # DNS settings for NSD/Unbound
 nsd_zones:
   - name: "{{ site_suffix }}"
-    ipv4_first_octets: "{{ mgmt_ipv4_first_octets }}"
-    name_reverse_unbound: "{{ mgmt_name_reverse_unbound }}"
+    cidr: "{{ management_net_cidr }}"
     soa: ns1
     ns:
       - { name: ns1 }
diff --git a/roles/dns-unbound/tasks/main.yml b/roles/dns-unbound/tasks/main.yml
index d2fe1a6..c4672d0 100644
--- a/roles/dns-unbound/tasks/main.yml
+++ b/roles/dns-unbound/tasks/main.yml
@@ -23,6 +23,16 @@
     cache_valid_time: 3600
   with_items:
     - unbound
+  register: unbound_install
+
+- name: Stop unbound until configured
+  when: unbound_install.changed
+  service:
+    name: unbound
+    enabled: no
+    state: stopped
+  tags:
+    - skip_ansible_lint # need to down service before configured
 
 - name: create unbound.conf from template
   template:
@@ -33,7 +43,6 @@
     group: "{{ unbound_group }}"
     # validate='unbound-checkconf %s' - can't use, checks path, not just config.
   notify:
-   - start-unbound
    - reload-unbound
 
 - name: flush unbound handlers
diff --git a/roles/dns-unbound/templates/unbound.conf.j2 b/roles/dns-unbound/templates/unbound.conf.j2
index 59c44e9..121a706 100644
--- a/roles/dns-unbound/templates/unbound.conf.j2
+++ b/roles/dns-unbound/templates/unbound.conf.j2
@@ -1,4 +1,4 @@
-
+# created by dns-unbound/templates/unbound.conf.j2
 {#
 Copyright 2017-present Open Networking Foundation
 
@@ -15,21 +15,21 @@
 limitations under the License.
 #}
 
-
-# unbound.conf (configured by Ansible)
-
 server:
 {% if unbound_listen_on_default %}
-  interface: {{ ansible_default_ipv4.address }}  
+  # created by unbound_listen_on_default
+  interface: {{ ansible_default_ipv4.address }}
 {% endif %}
-{% if unbound_interfaces is defined %}
-{% for cidr_ipv4 in unbound_interfaces %}
-  interface: {{ cidr_ipv4 | ipaddr('address') }}
+{% if unbound_listen_zones %}
+{% for zone in nsd_zones %}
+  # created by nsd_zones: {{ zone.name }}
+  interface: {{ zone.cidr | ipaddr('1') | ipaddr('address') }}
 {% endfor %}
 {% endif %}
   verbosity: 1
   port: 53
   do-ip4: yes
+  do-ip6: no
   do-udp: yes
   do-tcp: yes
 
@@ -41,36 +41,29 @@
   access-control: 0.0.0.0/0 allow
 {% endif %}
 
-{% if unbound_listen_on_default %}
-  # allow from default interfaces
-  access-control: {{ ansible_default_ipv4.address }}/{{ (ansible_default_ipv4.address ~ "/" ~ ansible_default_ipv4.netmask) | ipaddr('prefix') }} allow
-{% endif %}
-
-{% if unbound_interfaces is defined %}
-  # allow from local networks
-{% for cidr_ipv4 in unbound_interfaces %}
-  access-control: {{ cidr_ipv4 | ipaddr('0') }} allow
+{% if nsd_zones %}
+  # allow from networks defined in zones
+{% for zone in nsd_zones %}
+  access-control: {{ zone.cidr | ipaddr('0') }} allow
 {% endfor %}
-{% endif %}
 
-{% if nsd_zones is defined %}
 # allow unbound to query localhost, where nsd is listening
 do-not-query-localhost: no
 
 # allow reverse queries for RFC1918 addresses
 {% for zone in nsd_zones %}
-local-zone: "{{ zone.name_reverse_unbound }}." nodefault
+local-zone: "{{ zone.cidr | unbound_revdns }}" nodefault
 {% endfor %}
 
 # stub-zones zones that nsd is serving
 {% for zone in nsd_zones %}
 stub-zone:
   name: "{{ zone.name }}"
-  stub-addr: {{ nsd_ip | default("127.0.0.1") }}
+  stub-addr: {{ nsd_ip }}
 
 stub-zone:
-  name: "{{ zone.name_reverse_unbound }}."
-  stub-addr: {{ nsd_ip | default("127.0.0.1") }}
+  name: "{{ zone.cidr | unbound_revdns }}"
+  stub-addr: {{ nsd_ip }}
 
 {% endfor %}
 {% endif %}
diff --git a/roles/head-mgmtbr/defaults/main.yml b/roles/head-mgmtbr/defaults/main.yml
deleted file mode 100644
index 8bfb1a1..0000000
--- a/roles/head-mgmtbr/defaults/main.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-# Copyright 2017-present Open Networking Foundation
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
----
-# head-mgmtbr/defaults/main.yml
-
-# public internet facing NAT interface
-mgmtbr_nat_interface: eth0
-
-# management interface bridged to mgmtbr
-mgmtbr_ext_interface: eth1
diff --git a/roles/head-mgmtbr/tasks/main.yml b/roles/head-mgmtbr/tasks/main.yml
deleted file mode 100644
index 63314e4..0000000
--- a/roles/head-mgmtbr/tasks/main.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-# Copyright 2017-present Open Networking Foundation
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
----
-# head-mgmtbr/tasks/main.yml
-
-- name: Create mgmtbr bridge configuration
-  template:
-    src: "mgmtbr.cfg.j2"
-    dest: /etc/network/interfaces.d/mgmtbr.cfg
-    owner: root
-    group: root
-    mode: 0644
-  register: mgmtbr_config
-
-- name: Bring up mgmtbr if reconfigured
-  when: mgmtbr_config.changed and ansible_mgmtbr is not defined
-  command: ifup mgmtbr
-  tags:
-    - skip_ansible_lint # needs to be run here or the next steps will fail
-
-- name: Default to accept forwarded traffic
-  iptables:
-    chain: FORWARD
-    policy: ACCEPT
-
-- name: Configure NAT for mgmtbr
-  iptables:
-    table: nat
-    chain: POSTROUTING
-    out_interface: "{{ mgmtbr_nat_interface }}"
-    jump: MASQUERADE
-
-- name: Configure forwarding for mgmtbr
-  iptables:
-    chain: FORWARD
-    in_interface: mgmtbr
-    jump: ACCEPT
-
diff --git a/roles/head-mgmtbr/templates/mgmtbr.cfg.j2 b/roles/head-mgmtbr/templates/mgmtbr.cfg.j2
deleted file mode 100644
index 374fefc..0000000
--- a/roles/head-mgmtbr/templates/mgmtbr.cfg.j2
+++ /dev/null
@@ -1,29 +0,0 @@
-
-{#
-Copyright 2017-present Open Networking Foundation
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-#}
-
-
-auto mgmtbr
-iface mgmtbr inet static
-    address {{ nsd_zones[0].ipv4_first_octets }}.1
-    network {{ nsd_zones[0].ipv4_first_octets }}.0
-    netmask 255.255.255.0
-    broadcast {{ nsd_zones[0].ipv4_first_octets }}.255
-    gateway {{ nsd_zones[0].ipv4_first_octets }}.1
-    bridge_ports {{ mgmtbr_ext_interface }}
-    dns-search {{ site_suffix }}
-    dns-nameservers {{ dns_servers | join(" ") }}
-
diff --git a/roles/interface-config/defaults/main.yml b/roles/interface-config/defaults/main.yml
index e1039fd..8dcbf0e 100644
--- a/roles/interface-config/defaults/main.yml
+++ b/roles/interface-config/defaults/main.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +12,41 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
-
----
+#
 # interface-config/defaults/main.yml
 
-mgmt_interface: eth1
-
-mgmt_ipv4_first_octets: "192.168.200"
-
+# list of physical nodes in the scenario
 physical_node_list: []
 
+# headnode internet-facing interface to NAT mgmtbridge traffic out of
+headnode_nat_interface: eth0
+
+# network interfaces on  physical nodes
+management_net_interfaces: []
+fabric_net_interfaces: []
+
+# management network configuration
+management_net_cidr: "192.168.200.0/24"
+dns_servers:
+  - "{{ management_net_cidr | ipaddr('1') | ipaddr('address') }}"
+
+# VTN MANAGEMENT_HOST network
+use_vtn_net_management_host: False
+vtn_net_management_host_cidr: "{{ management_net_cidr }}"
+vtn_net_management_host_hwaddr_prefix: "06A6"
+
+# VTN PUBLIC network, used with fabric
+use_vtn_net_fabric: False
+vtn_data_plane_interface: "vethfabric1"
+vtn_net_public_cidr: "10.6.1.0/24"
+vtn_net_public_hwaddr_prefix: "0242"
+
+# VSG and public address pools
+use_addresspool_vsg: False
+addresspool_vsg_cidr: "10.7.1.0/24"
+addresspool_vsg_hwaddr_prefix: "0ACA"
+
+use_addresspool_public: False
+addresspool_public_cidr: "10.8.1.0/24"
+addresspool_public_hwaddr_prefix: "0EFE"
+
diff --git a/roles/interface-config/handlers/main.yml b/roles/interface-config/handlers/main.yml
new file mode 100644
index 0000000..13b765b
--- /dev/null
+++ b/roles/interface-config/handlers/main.yml
@@ -0,0 +1,10 @@
+---
+# interface-config/handlers/main.yml
+
+# the next handler may need to change in 16.04 as iptables-persistent was split
+# and may need 'netfilter-persistent' installed as well
+- name: iptables-save
+  shell: iptables-save | grep -vi docker > /etc/iptables/rules.v4
+  tags:
+    - skip_ansible_lint
+
diff --git a/roles/interface-config/tasks/main.yml b/roles/interface-config/tasks/main.yml
index f768c38..44efe78 100644
--- a/roles/interface-config/tasks/main.yml
+++ b/roles/interface-config/tasks/main.yml
@@ -1,4 +1,4 @@
-
+---
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,22 +13,90 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
----
 # interface-config/tasks/main.yml
 
-- name: Create network interface for management network
+- name: Install bridging/bonding utilities
+  apt:
+    name: "{{ item }}"
+    update_cache: yes
+    cache_valid_time: 3600
+  with_items:
+    - bridge-utils
+    - ifenslave
+    - iptables-persistent
+
+- name: Create management network interfaces
   template:
-    src: eth.cfg.j2
-    dest: "/etc/network/interfaces.d/{{ mgmt_interface }}.cfg"
+    src: management.cfg.j2
+    dest: "/etc/network/interfaces.d/management.cfg"
     owner: root
     group: root
     mode: 0644
-  register: mgmtint_config
+  register: management_net_config
 
-- name: Bring up management network if reconfigured
-  when: mgmtint_config.changed
-  command: "ifup {{ mgmt_interface }}"
+- name: Bring up management network interfaces, if reconfigured
+  when: management_net_config.changed
+  command: "ifup {{ item }}"
+  with_flattened:
+   - mgmtbridge
+   - mgmtbond
+   - "{{ management_net_interfaces }}"
+   - vethmgmt0
+  tags:
+    - skip_ansible_lint # needs to be run before next steps
+
+# NAT/forward management network traffic out the head node
+- name: Default to accept forwarded traffic
+  when: "'head' in group_names and management_net_config.changed"
+  iptables:
+    chain: FORWARD
+    policy: ACCEPT
+  notify:
+    - iptables-save
+  tags:
+    - skip_ansible_lint # need to save config in following steps
+
+- name: Configure forwarding for management bridge
+  when: "'head' in group_names and management_net_config.changed"
+  iptables:
+    chain: FORWARD
+    in_interface: mgmtbridge
+    jump: ACCEPT
+  notify:
+    - iptables-save
+  tags:
+    - skip_ansible_lint # need to save config in following steps
+
+- name: Configure NAT for management network
+  when: "'head' in group_names and management_net_config.changed"
+  iptables:
+    table: nat
+    chain: POSTROUTING
+    out_interface: "{{ headnode_nat_interface }}"
+    jump: MASQUERADE
+  notify:
+    - iptables-save
+  tags:
+    - skip_ansible_lint # need to save config in following steps
+
+# Create fabric bridge and veth pair
+- name: Create fabric network interfaces on compute nodes
+  template:
+    src: fabric.cfg.j2
+    dest: "/etc/network/interfaces.d/fabric.cfg"
+    owner: root
+    group: root
+    mode: 0644
+  register: compute_fabric_config
+
+- name: Bring up fabric interfaces, if reconfigured
+  when: compute_fabric_config.changed
+  command: "ifup {{ item }}"
+  with_flattened:
+    - fabricbridge
+    - fabricbond
+    - "{{ fabric_net_interfaces }}"
+    - vethfabric0
   tags:
     - skip_ansible_lint # needs to be run before next steps
 
diff --git a/roles/interface-config/templates/eth.cfg.j2 b/roles/interface-config/templates/eth.cfg.j2
deleted file mode 100644
index b9aa67d..0000000
--- a/roles/interface-config/templates/eth.cfg.j2
+++ /dev/null
@@ -1,30 +0,0 @@
-
-{#
-Copyright 2017-present Open Networking Foundation
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-#}
-
-
-{% for node in physical_node_list if node.name == ansible_hostname %}
-auto {{ node.interface | default(mgmt_interface) }}
-
-iface {{ node.interface | default(mgmt_interface) }} inet static
-    address {{ mgmt_ipv4_first_octets }}.{{ node.ipv4_last_octet }}
-    network {{ mgmt_ipv4_first_octets }}.0
-    netmask 255.255.255.0
-    broadcast {{ mgmt_ipv4_first_octets }}.255
-{% if node.gateway_enabled is defined and node.gateway_enabled %}
-    gateway {{ mgmt_ipv4_first_octets }}.1
-{% endif %}
-{% endfor %}
diff --git a/roles/interface-config/templates/fabric.cfg.j2 b/roles/interface-config/templates/fabric.cfg.j2
new file mode 100644
index 0000000..0547f6a
--- /dev/null
+++ b/roles/interface-config/templates/fabric.cfg.j2
@@ -0,0 +1,83 @@
+# Created by platform-install: interface-config/templates/fabric.cfg.j2
+{% for node in physical_node_list if node.name == ansible_hostname %}
+
+# fabricbridge between physical bond and virtual interfaces for VTN
+auto fabricbridge
+iface fabricbridge inet manual
+  pre-up ip link add fabricbridge type bridge
+  bridge_ports fabricbond vethfabric0
+
+# fabric bond of physical interfaces for VTN
+auto fabricbond
+iface fabricbond inet manual
+  pre-up ip link add fabricbond type bond
+  pre-up ip link set fabricbond up
+  bond-miimon 100
+  bond-slaves none
+  bond-mode active-backup
+  post-down ip link del fabricbond
+
+{% if fabric_net_interfaces %}
+# physical network members of fabricbond
+{% for fab_int in fabric_net_interfaces %}
+auto {{ fab_int }}
+iface {{ fab_int }} inet manual
+  pre-up ip link set {{ mgmt_int }} master fabricbond
+  bond-master fabricbond
+  bond-mode active-backup
+  bond-primary {{ management_net_interfaces | join(' ') }}
+  post-down ip link set dev {{ mgmt_int }} nomaster
+
+{% endfor %}
+{% endif %}
+
+# vethfabric0/vethfabric1 interfaces connect from VTN br-int to fabricbridge
+# vethfabric0: connected to fabricbridge
+auto vethfabric0
+iface vethfabric0 inet manual
+  pre-up ip link add vethfabric0 type veth peer name vethfabric1
+  pre-up ip link set vethfabric0 up
+  post-up ip link set dev vethfabric0 master fabricbridge
+  pre-down ip link set dev vethfabric0 nomaster
+  post-down ip link del vethfabric0
+
+# vethfabric1: becomes a part of br-int, which takes over the IP address
+{% set vtn_veth_ip = ( vtn_net_public_cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address')) %}
+auto vethfabric1
+iface vethfabric1 inet static
+  address {{ vtn_veth_ip }}
+  network {{ vtn_net_public_cidr | ipaddr('network') }}
+  netmask {{ vtn_net_public_cidr | ipaddr('netmask') }}
+  gateway {{ vtn_net_public_cidr | ipaddr('1') | ipaddr('address') }}
+  broadcast {{ vtn_net_public_cidr | ipaddr('broadcast') }}
+  hwaddress ether {{ ( vtn_net_public_hwaddr_prefix ~ ( vtn_veth_ip | ip4_hex )) | hwaddr('unix') }}
+
+{% if use_addresspool_vsg %}
+# vSG public gateway
+{% set ap_vsg_veth_ip = ( addresspool_vsg_cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address')) %}
+auto vethfabric1:0
+iface vethfabric1:0 inet static
+  address {{ ap_vsg_veth_ip }}
+  network {{ addresspool_vsg_cidr | ipaddr('network') }}
+  netmask {{ addresspool_vsg_cidr | ipaddr('netmask') }}
+  gateway {{ addresspool_vsg_cidr | ipaddr('1') | ipaddr('address') }}
+  broadcast {{ addresspool_vsg_cidr | ipaddr('broadcast') }}
+  hwaddress ether {{ ( addresspool_vsg_hwaddr_prefix ~ ( ap_vsg_eth_ip | ip4_hex )) | hwaddr('unix') }}
+
+{% endif %}
+
+{% if use_addresspool_public %}
+# public network gateway
+{% set ap_pub_veth_ip = ( addresspool_public_cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address')) %}
+auto vethfabric1:1
+iface vethfabric1:1 inet static
+  address {{ ap_pub_veth_ip }}
+  network {{ addresspool_public_cidr | ipaddr('network') }}
+  netmask {{ addresspool_public_cidr | ipaddr('netmask') }}
+  gateway {{ addresspool_public_cidr | ipaddr('1') | ipaddr('address') }}
+  broadcast {{ addresspool_public_cidr | ipaddr('broadcast') }}
+  hwaddress ether {{ ( addresspool_public_hwaddr_prefix ~ ( ap_pub_veth_ip | ip4_hex )) | hwaddr('unix') }}
+
+{% endif %}
+{% endfor %}
+
diff --git a/roles/interface-config/templates/management.cfg.j2 b/roles/interface-config/templates/management.cfg.j2
new file mode 100644
index 0000000..bb3c1e6
--- /dev/null
+++ b/roles/interface-config/templates/management.cfg.j2
@@ -0,0 +1,57 @@
+# Created by platform-install: interface-config/templates/management.cfg.j2
+{% for node in physical_node_list if node.name == ansible_hostname %}
+
+# management bridge between physical and virtual interfaces for VTN
+{% set mgmtbr_ip = ( vtn_net_management_host_cidr | ipaddr(node.ipv4_last_octet) | ipaddr('address')) %}
+auto mgmtbridge
+iface mgmtbridge inet static
+  pre-up ip link add mgmtbridge type bridge
+  bridge_ports mgmtbond vethmgmt0
+  address {{ mgmtbr_ip }}
+  network {{ vtn_net_management_host_cidr | ipaddr('network') }}
+  netmask {{ vtn_net_management_host_cidr | ipaddr('netmask') }}
+  broadcast {{ vtn_net_management_host_cidr | ipaddr('broadcast') }}
+  hwaddress ether {{ ( vtn_net_management_host_hwaddr_prefix ~ ( mgmtbr_ip | ip4_hex )) | hwaddr('unix') }}
+  dns-search {{ site_suffix }}
+  dns-nameservers {{ dns_servers | join(" ") }}
+
+# management bond of physical interfaces
+auto mgmtbond
+iface mgmtbond inet manual
+  pre-up ip link add mgmtbond type bond
+  pre-up ip link set mgmtbond up
+  bond-slaves none
+  bond-mode active-backup
+  bond-miimon 100
+  post-down ip link del mgmtbond
+
+{% if management_net_interfaces %}
+# physical network members of mgmtbond
+{% for mgmt_int in management_net_interfaces %}
+auto {{ mgmt_int }}
+iface {{ mgmt_int }} inet manual
+  pre-up ip link set {{ mgmt_int }} master mgmtbond
+  bond-master mgmtbond
+  bond-mode active-backup
+  bond-primary {{ management_net_interfaces | join(' ') }}
+  post-down ip link set dev {{ mgmt_int }} nomaster
+
+{% endfor %}
+{% endif %}
+
+# veth interfaces for VTN MANAGEMENT_HOST
+# vethmgmt0: connected to mgmtbond
+auto vethmgmt0
+iface vethmgmt0 inet manual
+  pre-up ip link add vethmgmt0 type veth peer name vethmgmt1
+  pre-up ip link set vethmgmt0 up
+  pre-up ip link set vethmgmt1 up
+  post-up ip link set dev vethmgmt0 master mgmtbridge
+  pre-down ip link set dev vethmgmt0 nomaster
+  post-down ip link del vethmgmt0
+
+# vethmgmt1: connected to VTN as hostManagementIface, if enabled
+# Brought up by vethmgmt0
+
+{% endfor %}
+