CORD-632 management DHCP range custom configuration

Change-Id: Ia808178f2922ad639a79e6a3e2e8a519a35e6464
diff --git a/bootstrap/bootstrap.py b/bootstrap/bootstrap.py
index c7a0826..f7d074c 100755
--- a/bootstrap/bootstrap.py
+++ b/bootstrap/bootstrap.py
@@ -16,7 +16,7 @@
                             auth=client._oauth(),
                             data=params)
 
-def add_or_update_node_group_interface(client, ng, gw, foundIfc, ifcName, subnet):
+def add_or_update_node_group_interface(client, ng, gw, foundIfc, ifcName, subnet, low, high):
     ip = ipaddress.IPv4Network(unicode(subnet, 'utf-8'))
     hosts = list(ip.hosts())
 
@@ -24,10 +24,10 @@
     gw = gw or str(hosts[0])
 
     ifc = {
-        'ip_range_high': str(hosts[-1]),
-        'ip_range_low': str(hosts[2]),
-	'static_ip_range_high' : None,
-	'static_ip_range_low' : None,
+        'ip_range_low': low if low != "" else str(hosts[2]),
+        'ip_range_high': high if high != "" else str(hosts[-1]),
+        'static_ip_range_high' : None,
+        'static_ip_range_low' : None,
         'management': 2,
         'name': ifcName,
         #'router_ip' : gw,
@@ -41,46 +41,46 @@
     if foundIfc is not None:
         print("INFO: network for specified interface, '%s', already exists" % (ifcName))
 
-	resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', dict())
-	if int(resp.status_code / 100) != 2:
-	    print("ERROR: unable to read specified interface, '%s', '%d : %s'"
-	          % (ifcName, resp.status_code, resp.text), file=sys.stderr)
-	    sys.exit(1)
+        resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', dict())
+        if int(resp.status_code / 100) != 2:
+            print("ERROR: unable to read specified interface, '%s', '%d : %s'"
+                  % (ifcName, resp.status_code, resp.text), file=sys.stderr)
+            sys.exit(1)
 
-	# A bit of a hack here. Turns out MAAS won't return the router_ip / gateway_ip value
-	# so we can't really tell if that value is set correctly. So we will compare the
-	# values we can and use that as the "CHANGED" value, but always set all values.
+        # A bit of a hack here. Turns out MAAS won't return the router_ip / gateway_ip value
+        # so we can't really tell if that value is set correctly. So we will compare the
+        # values we can and use that as the "CHANGED" value, but always set all values.
 
-	# Save the compare value
-	same = ifc == json.loads(resp.text)
+        # Save the compare value
+        same = ifc == json.loads(resp.text)
 
-	# Add router_ip and gateway_ip to the desired state so that those will be set
-	ifc['router_ip'] = gw
-	ifc['gateway_ip'] = gw
+        # Add router_ip and gateway_ip to the desired state so that those will be set
+        ifc['router_ip'] = gw
+        ifc['gateway_ip'] = gw
 
         # If the network already exists, update it with the information we want
         resp = put(client, '/nodegroups/' + ng['uuid'] + '/interfaces/' + ifcName + '/', ifc)
         if int(resp.status_code / 100) != 2:
             print("ERROR: unable to update specified network, '%s', on specified interface '%s', '%d : %s'"
                    % (subnet, ifcName, resp.status_code, resp.text), file=sys.stderr)
-	    sys.exit(1)
+            sys.exit(1)
 
         if not same:
             print("CHANGED: updated network, '%s', for interface '%s'" % (subnet, ifcName))
-	else:
+        else:
             print("INFO: Network settings for interface '%s' unchanged" % ifcName)
 
     else:
         # Add the operation
         ifc['op'] = 'new'
-	ifc['router_ip'] = gw
+        ifc['router_ip'] = gw
         ifc['gateway_ip'] = gw
 
         resp = client.post('/nodegroups/' + ng['uuid'] + '/interfaces/', ifc)
         if int(resp.status_code / 100) != 2:
             print("ERROR: unable to create specified network, '%s', on specified interface '%s', '%d : %s'"
                 % (subnet, ifcName, resp.status_code, resp.text), file=sys.stderr)
-	    sys.exit(1)
+            sys.exit(1)
         else:
             print("CHANGED: created network, '%s', for interface '%s'" % (subnet, ifcName))
 
@@ -89,7 +89,7 @@
     resp = client.get('/subnets/', dict())
     if int(resp.status_code / 100) != 2:
         print("ERROR: unable to query subnets: '%d : %s'" % (resp.status_code, resp.text))
-	sys.exit(1)
+        sys.exit(1)
     else:
         subnets = json.loads(resp.text)
 
@@ -101,30 +101,30 @@
 
     if id == None:
         print("ERROR: unable to find subnet entry for network '%s'" % (subnet))
-	sys.exit(1)
+        sys.exit(1)
 
     resp = client.get('/subnets/' + id + '/')
     if int(resp.status_code / 100) != 2:
         print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text))
-	sys.exit(1)
+        sys.exit(1)
 
     data = json.loads(resp.text)
 
     found = False
     for ns in data['dns_servers']:
         if unicode(ns) == unicode(hosts[0]):
-	    found = True
+            found = True
 
     if not found:
         resp = put(client, '/subnets/' + id + '/', dict(dns_servers=[hosts[0]]))
-	if int(resp.status_code / 100) != 2:
+        if int(resp.status_code / 100) != 2:
             print("ERROR: unable to query subnet '%s': '%d : %s'" % (subnet, resp.status_code, resp.text))
-	    sys.exit(1)
-	else:
-	    print("CHANGED: updated DNS server information")
+            sys.exit(1)
+        else:
+            print("CHANGED: updated DNS server information")
     else:
         print("INFO: DNS already set correctly")
-    
+
 
 def main():
     parser = OptionParser()
@@ -140,6 +140,10 @@
         help="the interface on which to set up DHCP for POD local hosts")
     parser.add_option('-n', '--network', dest='network', default='10.0.0.0/16',
         help="subnet to use for POD local DHCP")
+    parser.add_option('-l', '--network-low', dest='network_low', default='',
+        help="low address in network to lease via DHCP")
+    parser.add_option('-H', '--network-high', dest='network_high', default='',
+        help="high address in network to lease via DHCP")
     parser.add_option('-b', '--bridge', dest='bridge', default='mgmtbr',
         help="bridge to use for host local VM allocation")
     parser.add_option('-t', '--bridge-subnet', dest='bridge_subnet', default='172.18.0.0/16',
@@ -179,6 +183,10 @@
         config['interface'] = options.interface
     if options.network != None:
         config['network'] = options.network
+    if options.network_low != None:
+        config['network_low'] = options.network_low
+    if options.network_high != None:
+        config['network_high'] = options.network_high
     if options.bridge != None:
         config['bridge'] = options.bridge
     if options.bridge_subnet != None:
@@ -196,21 +204,21 @@
         sys.exit(1)
     else:
         config['sshkey'] = options.sshkey
-    
+
     auth = MaasAuth(config['url'], config['key'])
     client = MaasClient(auth)
 
     resp = client.get('/account/prefs/sshkeys/', dict(op='list'))
     if int(resp.status_code / 100) != 2:
         print("ERROR: unable to query SSH keys from server '%d : %s'"
-	        % (resp.status_code, resp.text), file=sys.stderr)
-	sys.exit(1)
+                % (resp.status_code, resp.text), file=sys.stderr)
+        sys.exit(1)
 
     found_key = False
     keys = json.loads(resp.text)
     for key in keys:
-	if key['key'] == config['sshkey']:
-	    print("INFO: specified SSH key already exists")
+        if key['key'] == config['sshkey']:
+            print("INFO: specified SSH key already exists")
             found_key = True
 
     # Add the SSH key to the user
@@ -221,9 +229,9 @@
             print("ERROR: unable to add sshkey for user: '%d : %s'"
                     % (resp.status_code, resp.text), file=sys.stderr)
             sys.exit(1)
-	else:
-	    print("CHANGED: updated ssh key")
-    
+        else:
+            print("CHANGED: updated ssh key")
+
     # Check to see if an "administrative" zone exists and if not
     # create one
     found = None
@@ -231,7 +239,7 @@
     for zone in zones:
         if zone['name'] == config['zone']:
             found=zone
-    
+
     if found is not None:
         print("INFO: administrative zone, '%s', already exists" % config['zone'], file=sys.stderr)
     else:
@@ -240,7 +248,7 @@
             sys.exit(1)
         else:
             print("CHANGED: Zone '%s' created" % config['zone'])
-    
+
     # If the interface doesn't already exist in the cluster then
     # create it. Look for the "Cluster Master" node group, but
     # if it is not found used the first one in the list, if the
@@ -251,7 +259,7 @@
         if ng['cluster_name'] == config['cluster']:
             found = ng
             break
-    
+
     if found is None:
         print("ERROR: unable to find cluster with specified name, '%s'" % config['cluster'], file=sys.stderr)
         sys.exit(1)
@@ -259,23 +267,23 @@
     resp = client.get('/nodegroups/' + ng['uuid'] + '/', dict())
     if int(resp.status_code / 100) != 2:
         print("ERROR: unable to get node group information for cluster '%s': '%d : %s'"
-	    % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
-	sys.exit(1)
+            % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
+        sys.exit(1)
 
     data = json.loads(resp.text)
-    
+
     # Set the DNS domain name (zone) for the cluster
     if data['name'] != config['domain']:
         resp = put(client, '/nodegroups/' + ng['uuid'] + '/', dict(name=config['domain']))
         if int(resp.status_code / 100) != 2:
             print("ERROR: unable to set the DNS domain name for the cluster with specified name, '%s': '%d : %s'"
                 % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
-	    sys.exit(1)
+            sys.exit(1)
         else:
             print("CHANGE: updated name of cluster to '%s' : %s" % (config['domain'], resp))
     else:
         print("INFO: domain name already set")
-    
+
     found = None
     resp = client.get('/nodegroups/' + ng['uuid'] + '/interfaces/', dict(op='list'))
     if int(resp.status_code / 100) != 2:
@@ -284,12 +292,13 @@
         sys.exit(1)
     ifcs = json.loads(resp.text)
 
-    localIfc = hostIfc = None 
+    localIfc = hostIfc = None
     for ifc in ifcs:
         localIfc = ifc if ifc['name'] == config['interface'] else localIfc
         hostIfc = ifc if ifc['name'] == config['bridge'] else hostIfc
 
-    add_or_update_node_group_interface(client, ng, config['gw'], localIfc, config['interface'], config['network'])
+    add_or_update_node_group_interface(client, ng, config['gw'], localIfc, config['interface'], config['network'],
+         config['network_low'], config['network_high'])
     #add_or_update_node_group_interface(client, ng, config['gw'], hostIfc, config['bridge'], config['bridge-subnet'])
 
     # Update the server settings to upstream DNS request to Google
@@ -298,7 +307,7 @@
     if int(resp.status_code / 100) != 2:
         print("ERROR: unable to get the upstream DNS servers: '%d : %s'"
               % (resp.status_code, resp.text), file=sys.stderr)
-	sys.exit(1)
+        sys.exit(1)
 
     if unicode(json.loads(resp.text)) != u'8.8.8.8 8.8.8.4':
         resp = client.post('/maas/', dict(op='set_config', name='upstream_dns', value='8.8.8.8 8.8.8.4'))
@@ -314,27 +323,27 @@
     resp = client.get('/boot-resources/', None)
     if int(resp.status_code / 100) != 2:
         print("ERROR: unable to read existing images download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr)
-	sys.exit(1)
+        sys.exit(1)
 
     imgs = json.loads(resp.text)
     found = False
     for img in imgs:
-	if img['name'] == u'ubuntu/trusty' and img['architecture'] == u'amd64/hwe-t':
-	    found = True
+        if img['name'] == u'ubuntu/trusty' and img['architecture'] == u'amd64/hwe-t':
+            found = True
 
     if not found:
         resp = client.post('/boot-resources/', dict(op='import'))
         if int(resp.status_code / 100) != 2:
             print("ERROR: unable to start image download: '%d : %s'" % (resp.status_code, resp.text), file=sys.stderr)
-	    sys.exit(1)
+            sys.exit(1)
         else:
             print("CHANGED: Image download started")
     else:
-	print("INFO: required images already available")
-    
+        print("INFO: required images already available")
+
 if __name__ == '__main__':
     #try:
         main()
     #except:
-#	e = sys.exc_info()[0]
-#	print("ERROR: Unexpected exception: '%s'" % e, file=sys.stderr)
+#        e = sys.exc_info()[0]
+#        print("ERROR: Unexpected exception: '%s'" % e, file=sys.stderr)
diff --git a/build.gradle b/build.gradle
index c13f57d..48ed61d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -314,7 +314,11 @@
             .p(config.seedServer.password, "ansible_ssh_pass")
             .p(config.seedServer.sudoPassword, "ansible_sudo_pass")
             .p(config.seedServer.fabric_ip, "fabric_ip")
+            .p(config.seedServer.fabric_range_low, "fabric_range_low")
+            .p(config.seedServer.fabric_range_high, "fabric_range_high")
             .p(config.seedServer.management_ip, "management_ip")
+            .p(config.seedServer.management_range_low, "management_range_low")
+            .p(config.seedServer.management_range_high, "management_range_high")
             .p(config.seedServer.management_gw, "management_gw")
             .p(config.seedServer.management_bc, "management_bc")
             .p(config.seedServer.management_network, "management_network")
@@ -326,7 +330,7 @@
             .p(config.seedServer.external_iface, "external_iface")
             .p(config.seedServer.fabric_iface, "fabric_iface")
             .p(config.seedServer.domain, "domain")
-	    .p(config.seedServer.virtualbox_support, "virtualbox_support")
+            .p(config.seedServer.virtualbox_support, "virtualbox_support")
             .p(config.seedServer.power_helper_user, "power_helper_user")
             .p(config.seedServer.power_helper_host, "power_helper_host")
             .p(config.seedServer.port, "ansible_ssh_port")
@@ -365,19 +369,23 @@
             .p(config.seedServer.password, "ansible_ssh_pass")
             .p(config.seedServer.sudoPassword, "ansible_sudo_pass")
             .p(config.seedServer.fabric_ip, "fabric_ip")
-	    .p(config.seedServer.management_ip, "management_ip")
+            .p(config.seedServer.fabric_range_low, "fabric_range_low")
+            .p(config.seedServer.fabric_range_high, "fabric_range_high")
+            .p(config.seedServer.management_ip, "management_ip")
+            .p(config.seedServer.management_range_low, "management_range_low")
+            .p(config.seedServer.management_range_high, "management_range_high")
             .p(config.seedServer.management_gw, "management_gw")
             .p(config.seedServer.management_network, "management_network")
-	    .p(config.seedServer.management_iface, "management_iface")
-	    .p(config.seedServer.external_ip, "external_ip")
+            .p(config.seedServer.management_iface, "management_iface")
+            .p(config.seedServer.external_ip, "external_ip")
             .p(config.seedServer.external_gw, "external_gw")
             .p(config.seedServer.external_network, "external_network")
             .p(config.seedServer.external_iface, "external_iface")
-	    .p(config.seedServer.fabric_iface, "fabric_iface")
+            .p(config.seedServer.fabric_iface, "fabric_iface")
             .p(config.seedServer.domain, "domain")
             .p(config.seedServer.virtualbox_support, "virtualbox_support")
-	    .p(config.seedServer.power_helper_user, "power_helper_user")
-	    .p(config.seedServer.power_helper_host, "power_helper_host")
+            .p(config.seedServer.power_helper_user, "power_helper_user")
+            .p(config.seedServer.power_helper_host, "power_helper_host")
             .p(config.seedServer.port, "ansible_ssh_port")
     }
 
diff --git a/roles/maas/tasks/main.yml b/roles/maas/tasks/main.yml
index 7961f61..5e4ca7b 100644
--- a/roles/maas/tasks/main.yml
+++ b/roles/maas/tasks/main.yml
@@ -182,9 +182,15 @@
   tags:
     - maas_restart
 
+- name: Ensure latest bootstrap image
+  become: yes
+  docker_image:
+    name: docker-registry:5000/cord-maas-bootstrap:{{ docker.tag }}
+    pull: yes
+
 - name: Configure MAAS
   become: yes
-  command: docker run docker-registry:5000/cord-maas-bootstrap:{{ docker.tag }} --apikey='{{apikey.stdout}}' --sshkey='{{maas.user_sshkey}}' --url='http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0' --network='{{networks.management}}' --interface='{{interfaces.management}}' --zone='administrative' --cluster='Cluster master' --domain='{{maas.domain}}'
+  command: docker run docker-registry:5000/cord-maas-bootstrap:{{ docker.tag }} --apikey='{{apikey.stdout}}' --sshkey='{{maas.user_sshkey}}' --url='http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0' --network='{{networks.management}}' --network-low='{{ranges.management.low}}' --network-high='{{ranges.management.high}}' --interface='{{interfaces.management}}' --zone='administrative' --cluster='Cluster master' --domain='{{maas.domain}}'
   register: maas_config_result
   changed_when: maas_config_result.stdout.find("CHANGED") != -1
   failed_when: "maas_config_result.rc != 0 or 'ERROR' in maas_config_result.stdout"
diff --git a/roles/maas/vars/main.yml b/roles/maas/vars/main.yml
index 40f6f38..c9176ac 100644
--- a/roles/maas/vars/main.yml
+++ b/roles/maas/vars/main.yml
@@ -54,6 +54,9 @@
     fabric:
         low: "{{ fabric_range_low | default(fabric_ip | default('10.6.1.0/24')) | ipaddr(2) | ipaddr('address') }}"
         high: "{{ fabric_range_high | default(fabric_ip | default('10.6.1.0/24')) | ipaddr(-3) | ipaddr('address') }}"
+    management:
+        low: "{{ management_range_low | default(management_ip | default('10.6.0.0/24')) | ipaddr(2) | ipaddr('address') }}"
+        high: "{{ management_range_high | default(management_ip | default('10.6.0.0/24')) | ipaddr(-3) | ipaddr('address') }}"
 
 docker:
     tag: "{{ deploy_docker_tag | default('candidate') }}"