CORD-654 configure MAAS via ansible module

Change-Id: I9f1b3b83b41d2cf1e87660d4a50fd3267ad1679a
diff --git a/README.md b/README.md
index 0813706..f1836c7 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@
    - A 2 port 10G card is installed on the head node
    - Head Node has _Internet_ connectivity via the 10G interface named `eth3`
 
-   _Additionally configuration variables, including network IP addressing information can be found in 
+   _Additionally configuration variables, including network IP addressing information can be found in
    the file `vars/main.yml`._
 
 ### Configuration
diff --git a/Vagrantfile b/Vagrantfile
index 8912e0f..5d03d83 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -69,5 +69,5 @@
   if Vagrant.has_plugin?("vagrant-cachier")
     config.cache.scope = :box
   end
-  
+
 end
diff --git a/automation/sample-filter.json b/automation/sample-filter.json
index 2a81a99..9d424df 100644
--- a/automation/sample-filter.json
+++ b/automation/sample-filter.json
@@ -1,11 +1,11 @@
-{  
-   "hosts":{  
-      "include":[  
+{
+   "hosts":{
+      "include":[
          ".*"
       ]
    },
-   "zones":{  
-      "include":[  
+   "zones":{
+      "include":[
          "default",
          "petaluma-lab"
       ]
diff --git a/bootstrap/Dockerfile b/bootstrap/Dockerfile
deleted file mode 100644
index f6a4598..0000000
--- a/bootstrap/Dockerfile
+++ /dev/null
@@ -1,32 +0,0 @@
-## Copyright 2016 Open Networking Laboratory
-##
-## 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.
-FROM ubuntu:14.04
-MAINTAINER Open Networking Laboratory <info@onlab.us>
-
-RUN	apt-get update -y && \
-	apt-get install -y python-pip
-
-RUN	pip install maasclient==0.3 && \
-	pip install requests_oauthlib && \
-        pip install ipaddress
-
-ADD bootstrap.py /bootstrap.py
-
-LABEL org.label-schema.name="bootstrap" \
-      org.label-schema.description="Provides bootstrap configuration of MAAS for the CORD deployment" \
-      org.label-schema.vcs-url="https://gerrit.opencord.org/maas" \
-      org.label-schema.vendor="Open Networking Laboratory" \
-      org.label-schema.schema-version="1.0"
-
-ENTRYPOINT [ "/bootstrap.py" ]
diff --git a/bootstrap/LICENSE b/bootstrap/LICENSE
deleted file mode 100644
index 8dada3e..0000000
--- a/bootstrap/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "{}"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright {yyyy} {name of copyright owner}
-
-   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.
diff --git a/bootstrap/Makefile b/bootstrap/Makefile
deleted file mode 100644
index 49d7ff2..0000000
--- a/bootstrap/Makefile
+++ /dev/null
@@ -1,9 +0,0 @@
-help:
-	-@echo "Available actions"
-	-@echo "    docker      - builds the docker container"
-
-docker: bootstrap.py
-	docker build -t cord/maas-bootstrap:0.1-prerelease .
-
-run:
-	docker run -ti --rm=true cord/maas-bootstrap:0.1-prerelease --apikey=$(CORD_APIKEY) --sshkey="$(CORD_SSHKEY)" --url=$(CORD_URL)
diff --git a/bootstrap/bootstrap.py b/bootstrap/bootstrap.py
deleted file mode 100755
index f7d074c..0000000
--- a/bootstrap/bootstrap.py
+++ /dev/null
@@ -1,349 +0,0 @@
-#!/usr/bin/python
-
-from __future__ import print_function
-import sys
-import json
-import ipaddress
-import requests
-from optparse import OptionParser
-from maasclient.auth import MaasAuth
-from maasclient import MaasClient
-
-# For some reason the maasclient doesn't provide a put method. So
-# we will add it here
-def put(client, url, params=None):
-    return requests.put(url=client.auth.api_url + url,
-                            auth=client._oauth(),
-                            data=params)
-
-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())
-
-    # if the caller specified the default gateway then honor that, else used the default
-    gw = gw or str(hosts[0])
-
-    ifc = {
-        '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,
-        #'gateway_ip' : gw,
-        'ip': str(hosts[0]),
-        'subnet_mask': str(ip.netmask),
-        'broadcast_ip': str(ip.broadcast_address),
-        'interface': ifcName,
-    }
-
-    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)
-
-        # 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)
-
-        # 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)
-
-        if not same:
-            print("CHANGED: updated network, '%s', for interface '%s'" % (subnet, ifcName))
-        else:
-            print("INFO: Network settings for interface '%s' unchanged" % ifcName)
-
-    else:
-        # Add the operation
-        ifc['op'] = 'new'
-        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)
-        else:
-            print("CHANGED: created network, '%s', for interface '%s'" % (subnet, ifcName))
-
-    # Add the first host to the subnet as the dns_server
-    subnets = None
-    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)
-    else:
-        subnets = json.loads(resp.text)
-
-    id = None
-    for sn in subnets:
-        if sn['name'] == subnet:
-            id = str(sn['id'])
-            break
-
-    if id == None:
-        print("ERROR: unable to find subnet entry for network '%s'" % (subnet))
-        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)
-
-    data = json.loads(resp.text)
-
-    found = False
-    for ns in data['dns_servers']:
-        if unicode(ns) == unicode(hosts[0]):
-            found = True
-
-    if not found:
-        resp = put(client, '/subnets/' + id + '/', dict(dns_servers=[hosts[0]]))
-        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")
-    else:
-        print("INFO: DNS already set correctly")
-
-
-def main():
-    parser = OptionParser()
-    parser.add_option('-c', '--config', dest='config_file',
-        help="specifies file from which configuration should be read", metavar='FILE')
-    parser.add_option('-a', '--apikey', dest='apikey',
-        help="specifies the API key to use when accessing MAAS")
-    parser.add_option('-u', '--url', dest='url', default='http://localhost/MAAS/api/1.0',
-        help="specifies the URL on which to contact MAAS")
-    parser.add_option('-z', '--zone', dest='zone', default='administrative',
-        help="specifies the zone to create for manually managed hosts")
-    parser.add_option('-i', '--interface', dest='interface', default='eth0:1',
-        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',
-        help="subnet to assign from for bridged hosts")
-    parser.add_option('-r', '--cluster', dest='cluster', default='Cluster master',
-        help="name of cluster to user for POD / DHCP")
-    parser.add_option('-s', '--sshkey', dest='sshkey', default=None,
-        help="specifies public ssh key")
-    parser.add_option('-d', '--domain', dest='domain', default='cord.lab',
-        help="specifies the domain to configure in maas")
-    parser.add_option('-g', '--gateway', dest='gw', default=None,
-        help="specifies the gateway to configure for servers")
-    (options, args) = parser.parse_args()
-
-    if len(args) > 0:
-        print("unknown command line arguments specified", file=sys.stderr)
-        parser.print_help()
-        sys.exit(1)
-
-    # If a config file was specified then read the config from that
-    config = {}
-    if options.config_file != None:
-        with open(options.config_file) as config_file:
-            config = json.load(config_file)
-
-    # Override the config with any command line options
-    if options.apikey == None:
-        print("must specify a  MAAS API key", file=sys.stderr)
-        sys.exit(1)
-    else:
-        config['key'] = options.apikey
-    if options.url != None:
-        config['url'] = options.url
-    if options.zone != None:
-        config['zone'] = options.zone
-    if options.interface != None:
-        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:
-        config['bridge-subnet'] = options.bridge_subnet
-    if options.cluster != None:
-        config['cluster'] = options.cluster
-    if options.domain != None:
-        config['domain'] = options.domain
-    if options.gw != None:
-        config['gw'] = options.gw
-    if not 'gw' in config.keys():
-        config['gw'] = None
-    if options.sshkey == None:
-        print("must specify a SSH key to use for cord user", file=sys.stderr)
-        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)
-
-    found_key = False
-    keys = json.loads(resp.text)
-    for key in keys:
-        if key['key'] == config['sshkey']:
-            print("INFO: specified SSH key already exists")
-            found_key = True
-
-    # Add the SSH key to the user
-    # POST /api/2.0/account/prefs/sshkeys/ op=new
-    if not found_key:
-        resp = client.post('/account/prefs/sshkeys/', dict(op='new', key=config['sshkey']))
-        if int(resp.status_code / 100) != 2:
-            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")
-
-    # Check to see if an "administrative" zone exists and if not
-    # create one
-    found = None
-    zones = client.zones
-    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:
-        if not client.zone_new(config['zone'], "Zone for manually administrated nodes"):
-            print("ERROR: unable to create administrative zone '%s'" % config['zone'], file=sys.stderr)
-            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
-    # list is empty, error out
-    found = None
-    ngs = client.nodegroups
-    for ng in ngs:
-        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)
-
-    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)
-
-    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)
-        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:
-        print("ERROR: unable to fetch interfaces for cluster with specified name, '%s': '%d : %s'"
-            % (config['cluster'], resp.status_code, resp.text), file=sys.stderr)
-        sys.exit(1)
-    ifcs = json.loads(resp.text)
-
-    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'],
-         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
-    # POST /api/2.0/maas/ op=set_config
-    resp = client.get('/maas/', dict(op='get_config', name='upstream_dns'))
-    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)
-
-    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'))
-        if int(resp.status_code / 100) != 2:
-            print("ERROR: unable to set the upstream DNS servers: '%d : %s'"
-                % (resp.status_code, resp.text), file=sys.stderr)
-        else:
-            print("CHANGED: updated up stream DNS servers")
-    else:
-        print("INFO: Upstream DNS servers correct")
-
-    # Start the download of boot images
-    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)
-
-    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 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)
-        else:
-            print("CHANGED: Image download started")
-    else:
-        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)
diff --git a/build.gradle b/build.gradle
index 08f50ca..d5dac1f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -113,7 +113,7 @@
     }
     return branchStdOut.toString().trim()
 }
- 
+
 task buildSwitchqImage(type: Exec) {
     commandLine "docker", 'build', '--label', 'org.label-schema.build-date=' + getBuildTimestamp(), '--label', 'org.label-schema.vcs-ref=' + getCommitHash(), '--label', 'org.label-schema.vcs-ref-date=' + getCommitDate(), '--label', 'org.label-schema.version=' + getBranchName(), '-t', 'cord-maas-switchq', './switchq'
 }
@@ -128,22 +128,6 @@
     commandLine "docker", 'push', "$targetReg/cord-maas-switchq:$targetTag"
 }
 
-// Bootstrap Image
-
-task buildBootstrapImage(type: Exec) {
-    commandLine "docker", 'build', '--label', 'org.label-schema.build-date=' + getBuildTimestamp(), '--label', 'org.label-schema.vcs-ref=' + getCommitHash(), '--label', 'org.label-schema.vcs-ref-date=' + getCommitDate(), '--label', 'org.label-schema.version=' + getBranchName(), '-t', 'cord-maas-bootstrap', './bootstrap'
-}
-
-task tagBootstrapImage(type: Exec) {
-   dependsOn buildBootstrapImage
-   commandLine "docker", 'tag', 'cord-maas-bootstrap', "$targetReg/cord-maas-bootstrap:$targetTag"
-}
-
-task publishBootstrapImage(type: Exec) {
-    dependsOn tagBootstrapImage
-    commandLine "docker", 'push', "$targetReg/cord-maas-bootstrap:$targetTag"
-}
-
 // IP Allocator Image
 
 task buildAllocationImage(type: Exec) {
@@ -242,7 +226,6 @@
 // To be used to generate all needed binaries that need to be present on the target
 // as docker images in the local docker runner.
 task buildImages {
-    dependsOn buildBootstrapImage
     dependsOn buildHarvesterImage
     dependsOn buildAutomationImage
     dependsOn buildAllocationImage
@@ -252,7 +235,6 @@
 }
 
 task tagImages {
-    dependsOn tagBootstrapImage
     dependsOn tagHarvesterImage
     dependsOn tagAutomationImage
     dependsOn tagAllocationImage
@@ -264,7 +246,6 @@
 task publish {
     //FIXME: This works because the upstream project primes the nodes before running this.
     comps.each { name, spec -> if (spec.type == 'image') { dependsOn "publish" + name } }
-    dependsOn publishBootstrapImage
     dependsOn publishHarvesterImage
     dependsOn publishAutomationImage
     dependsOn publishAllocationImage
@@ -360,8 +341,8 @@
     extraVars = extraVars.p("$targetReg", "deploy_docker_registry")
         .p("$targetTag", "deploy_docker_tag")
 
-    // the password set on the compute node is skipped because this is being run against the 
-    // head node and we don't want to change the head node password as this node was manualy 
+    // the password set on the compute node is skipped because this is being run against the
+    // head node and we don't want to change the head node password as this node was manualy
     // set up.
     def skipTags = [].p(config.seedServer.skipTags).p('set_compute_node_password')
 
diff --git a/config-generator/README.md b/config-generator/README.md
index 7363057..8c9913b 100644
--- a/config-generator/README.md
+++ b/config-generator/README.md
@@ -6,7 +6,7 @@
 - Install Go (only for local debugging)
 - Make sure ONOS is reachable either locally or via SSH tunnel (either way is fine)
 - Make sure devices, hosts are connected and showing up in ONOS
-- Config-gen server listens on port 1337, so make sure it's available, if not change the ENV VAR `CONFIGGEN_ConfigServerPort` to the port you wish config-gen to use 
+- Config-gen server listens on port 1337, so make sure it's available, if not change the ENV VAR `CONFIGGEN_ConfigServerPort` to the port you wish config-gen to use
 
 
 ### To run locally from source (without a container) for debugging:
@@ -40,4 +40,4 @@
 	LogFormat        string `default:"text" envconfig:"LOG_FORMAT"`
 	ConfigServerPort string `default:"1337"`		//Config-gen service port default
 	ConfigServerIP   string `default:"127.0.0.1"`	//Config-gen service IP
-```
\ No newline at end of file
+```
diff --git a/library/maas.py b/library/maas.py
new file mode 100644
index 0000000..10cdbe7
--- /dev/null
+++ b/library/maas.py
@@ -0,0 +1,259 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas
+short_description: Manage MAAS server configuration
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  options:
+    description:
+      - list of config options to query, this is only used for query
+  enable_http_proxy: :
+    description:
+      - Enable the use of an APT and HTTP/HTTPS proxy.
+  upstream_dns: :
+    description:
+      - Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses).
+  default_storage_layout: :
+    description:
+      - Default storage layout.
+    choices:
+      - ['lvm', 'flat', 'bcache']
+  default_osystem: :
+    description:
+      - Default operating system used for deployment.
+  ports_archive: :
+    description:
+      - Ports archive.
+  http_proxy: :
+    description:
+      - Proxy for APT and HTTP/HTTPS.
+  boot_images_auto_import: :
+    description:
+      - Automatically import/refresh the boot images every 60 minutes.
+  enable_third_party_drivers: :
+    description:
+      - Enable the installation of proprietary drivers (i.e. HPVSA).
+  kernel_opts: :
+    description:
+      - Boot parameters to pass to the kernel by default.
+  main_archive: :
+    description:
+      - Main archive
+  maas_name: :
+    description:
+      - MAAS name.
+  curtin_verbose: :
+    description:
+      - Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..
+  dnssec_validation: :
+    description:
+      - Enable DNSSEC validation of upstream zones.
+  commissioning_distro_series: :
+    description:
+      - Default Ubuntu release used for commissioning.
+  windows_kms_host: :
+    description:
+      - Windows KMS activation host.
+  enable_disk_erasing_on_release: :
+    description:
+      - Erase nodes' disks prior to releasing..
+  default_distro_series: :
+    description:
+      - Default OS release used for deployment.
+  ntp_server: :
+    description:
+      - Address of NTP server for nodes.
+  default_min_hwe_kernel: :
+    description:
+      - Default Minimum Kernel Version.
+  state:
+    description:
+      - possible states for the module
+    choices: ['present', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    options:
+      - upstream_dns
+      - ntp_servers
+    state: query
+
+  maas:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    upstream_dns: 8.8.8.8 8.8.8.4
+    state: present
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            return True
+    return False
+
+# Get configuration options from MAAS
+def get_config(maas, desired):
+    config = {}
+    for o in desired.keys():
+        res = maas.get('/maas/', dict(name=o, op='get_config'))
+        if res.ok:
+            val = json.loads(res.text)
+            config[o] = val if val else ""
+        else:
+            config[o] = {'error': string_or_object(res.text)}
+    return config
+
+# Walk the list of options in the desired state setting those on MAAS
+def update_config(maas, have, want):
+    have_error = False
+    status = {}
+    for o in want.keys():
+        if want[o] != have[o]:
+            res = maas.post('/maas/', {'name': o, 'value': want[o], 'op': 'set_config'})
+            if res.ok:
+                status[o] = { 'error': False, 'status': want[o] }
+            else:
+                have_error = True
+                status[o] = { 'error': True, 'status': string_or_object(res.text) }
+    return {'error': have_error, 'status': status}
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            options=dict(required=False, type='list'),
+            enable_http_proxy=dict(required=False),
+            upstream_dns=dict(required=False),
+            default_storage_layout=dict(required=False),
+            default_osystem=dict(required=False),
+            ports_archive=dict(required=False),
+            http_proxy=dict(required=False),
+            boot_images_auto_import=dict(required=False),
+            enable_third_party_drivers=dict(required=False),
+            kernel_opts=dict(required=False),
+            main_archive=dict(required=False),
+            maas_name=dict(required=False),
+            curtin_verbose=dict(required=False),
+            dnssec_validation=dict(required=False),
+            commissioning_distro_series=dict(required=False),
+            windows_kms_host=dict(required=False),
+            enable_disk_erasing_on_release=dict(required=False),
+            default_distro_series=dict(required=False),
+            ntp_server=dict(required=False),
+            default_min_hwe_kernel=dict(required=False),
+            state=dict(default='present', choices=['present', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    options = module.params['options']
+    state = module.params['state']
+
+    if state == 'query':
+        desired = {x:None for x in options}
+    else:
+        # Construct a sparsely populate desired state
+        desired = remove_null({
+            'enable_http_proxy': module.params['enable_http_proxy'],
+            'upstream_dns': module.params['upstream_dns'],
+            'default_storage_layout': module.params['default_storage_layout'],
+            'default_osystem': module.params['default_osystem'],
+            'ports_archive': module.params['ports_archive'],
+            'http_proxy': module.params['http_proxy'],
+            'boot_images_auto_import': module.params['boot_images_auto_import'],
+            'enable_third_party_drivers': module.params['enable_third_party_drivers'],
+            'kernel_opts': module.params['kernel_opts'],
+            'main_archive': module.params['main_archive'],
+            'maas_name': module.params['maas_name'],
+            'curtin_verbose': module.params['curtin_verbose'],
+            'dnssec_validation': module.params['dnssec_validation'],
+            'commissioning_distro_series': module.params['commissioning_distro_series'],
+            'windows_kms_host': module.params['windows_kms_host'],
+            'enable_disk_erasing_on_release': module.params['enable_disk_erasing_on_release'],
+            'default_distro_series': module.params['default_distro_series'],
+            'ntp_server': module.params['ntp_server'],
+            'default_min_hwe_kernel': module.params['default_min_hwe_kernel'],
+        })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the configuration from MAAS
+    config = get_config(maas, desired)
+
+    if state == 'query':
+        # If this is a query, return the options
+        module.exit_json(changed=False, found=True, maas=config)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(config, desired):
+            res = update_config(maas, config, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, maas=res['status'])
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, maas=config)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_boot_resource.py b/library/maas_boot_resource.py
new file mode 100644
index 0000000..f81f3e4
--- /dev/null
+++ b/library/maas_boot_resource.py
@@ -0,0 +1,113 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_boot_resources
+short_description: Manage MAAS boot resources
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  state:
+    description:
+      - possible states for this sshkey
+    choices: ['query', 'import']
+    default: query
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_boot_resource:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    state: query
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+def filter(filter_type, d, keys):
+    if filter_type == 'include':
+        for k in d.keys():
+            if k not in keys:
+                d.pop(k, None)
+    else:
+        for k in d.keys():
+            if k in keys:
+                d.pop(k, None)
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            state=dict(default='query', choices=['query', 'import'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    if state == 'query':
+        res = maas.get('/boot-resources/')
+        if res.ok:
+            module.exit_json(changed=False, resources=json.loads(res.text))
+        else:
+            module.fail_json(msg=string_or_object(res.text))
+    elif state == 'import':
+        res = maas.post('/boot-resources/', dict(op='import'))
+        if res.ok:
+            module.exit_json(changed=True)
+        else:
+            module.fail_json(msg=string_or_object(res.text))
+    else:
+        module.fail_json(msg='unknown state')
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_cluster.py b/library/maas_cluster.py
new file mode 100644
index 0000000..5a3f4b7
--- /dev/null
+++ b/library/maas_cluster.py
@@ -0,0 +1,188 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_cluster
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  name:
+    description:
+      - name of the cluster
+    required: yes
+  status:
+    description:
+      - indicates the enabled state of the cluster
+    choices: ['enabled', 'disabled']
+    default: enabled
+  domain:
+    description:
+      - DNS zone name
+    required: no
+  state:
+    description:
+      - possible states for this cluster
+    choices: ['present', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_cluster:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyCluster
+    status: enabled
+    domain: company.com
+    state: present
+
+  maas_cluster:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyDeadCluster
+    state: query
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            return True
+    return False
+
+# Get an cluster from MAAS using its name, if not found return None
+def get_cluster(maas, name):
+    res = maas.get('/nodegroups/', dict(op='list'))
+    if res.ok:
+        for ng in json.loads(res.text):
+            if ng['cluster_name'] == name:
+                return ng
+    return None
+
+def update_cluster(maas, have, want):
+    merged = have.copy()
+    merged.update(want)
+    res = put(maas, '/nodegroups/%s/' % merged['uuid'], merged)
+    if res.ok:
+        return { 'error': False, 'status': get_cluster(maas, merged['cluster_name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            name=dict(required=True),
+            status=dict(default='enabled', choices=['enabled', 'disabled']),
+            domain=dict(required=False),
+            state=dict(default='present', choices=['present', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    status_map = {
+        'enabled': 1,
+        'disabled': 2
+    }
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'cluster_name': module.params['name'],
+        'status': status_map[module.params['status']],
+        'name' : module.params['domain'],
+    })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the cluster from MAAS
+    cluster = get_cluster(maas, desired['cluster_name'])
+
+    # Actions if the cluster does not currently exist
+    if not cluster:
+        if state == 'query':
+            # If this is a query, returne it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # Not able to create clusters via the API
+            module.fail_json(msg='Named cluster does not exist and clusters cannot be programatically created')
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with clusters does not exists actions
+        return
+
+    # Actions if the cluster does exist
+    if state == 'query':
+        # If this is a query, return the cluster
+        module.exit_json(changed=False, found=True, cluster=cluster)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(cluster, desired):
+            res = update_cluster(maas, cluster, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, cluster=res['status'])
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, cluster=cluster)
+    else:
+        # Not able to delete clusters via the API
+        module.fail_json(msg='Named cluster exists and clusters cannot be programmatically deleted')
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_cluster_interface.py b/library/maas_cluster_interface.py
new file mode 100644
index 0000000..9998692
--- /dev/null
+++ b/library/maas_cluster_interface.py
@@ -0,0 +1,302 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_cluster_interface
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  cluster_name:
+    description:
+      - name of the cluster for the interface
+    required: yes
+  name:
+    description:
+      - name of the cluster interface
+    required: yes
+  management:
+    description:
+      - indicates how or if MAAS manages this interface
+    choices: ['unmanaged', 'dhcp', 'dhcpdns']
+    default: unmanaged
+  interface:
+    description:
+      - the physical NIC for the interface
+    required: no
+  ip:
+    description:
+      - IP address assigned for this interface
+    required: no
+  subnet_mask:
+    description:
+      - network subnet mask for this interface
+    required: no
+  broadcast_ip:
+    description:
+      - broadcast IP for this interfaece's network
+    required: no
+  router_ip:
+    description:
+      - gateway router IP for this interface's network
+    required: no
+  ip_range_low:
+    description:
+      - the low range for dynamic IP address assignement
+    required: no
+  ip_range_high:
+    description:
+      - the high range for dynamic IP address assignment
+    required: no
+  static_ip_range_low:
+    description:
+      - the low range for static IP address assignment
+    required: no
+  static_ip_range_high:
+    description:
+      - the high range for static IP address assignment
+    required: no
+  state:
+    description:
+      - possible states for this cluster interface
+    choices: ['present', 'absent', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_cluster_interface:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyClusterInterface
+    interface: eth0
+    cluster_name: MyCluster
+    ip: 172.16.10.2
+    subnet_mask: 255.255.255.0
+    broadcast_ip: 172.16.10.255
+    router_ip: 172.16.10.1
+    ip_range_low: 172.16.10.3
+    ip_range_high: 172.16.10.127
+    static_ip_range_low: 172.16.10.128
+    static_ip_range_high: 172.16.10.253
+    management: dhcpdns
+    status: enabled
+    state: present
+
+  maas_cluster_interface:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyDeadClusterInterface
+    state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Deterine if two dictionaries are different
+def different(have, want, debug):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            diff = {"diff": key, "want": want[key]}
+            if key in have_keys:
+                diff['have'] = have[key]
+            else:
+                diff['have'] = False
+            debug.append(diff)
+            return True
+    return False
+
+# Get an cluster from MAAS using its name, if not found return None
+def get_cluster(maas, name):
+    res = maas.get('/nodegroups/', dict(op='list'))
+    if res.ok:
+        for ng in json.loads(res.text):
+            if ng['cluster_name'] == name:
+                return ng
+    return None
+
+# Get an cluster interface from MAAS using its name, if not found return None
+def get_cluster_interface(maas, cluster, name):
+    res = maas.get('/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], name))
+    if res.ok:
+        return json.loads(res.text)
+    return None
+
+# Create an cluster interface based on the value given
+def create_cluster_interface(maas, cluster, cluster_interface):
+    merged = cluster_interface.copy()
+    merged['op'] = 'new'
+    res = maas.post('/nodegroups/%s/interfaces/' % cluster['uuid'], merged)
+    if res.ok:
+        return { 'error': False, 'status': get_cluster_interface(maas, cluster, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an cluster interface based on the name
+def delete_cluster_interface(maas, cluster, name):
+    res = maas.delete('/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], name))
+    if res.ok:
+        return { 'error': False }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_cluster_interface(maas, cluster, have, want):
+    merged = have.copy()
+    merged.update(want)
+    res = put(maas, '/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], merged['name']), merged)
+    if res.ok:
+        return { 'error': False, 'status': get_cluster_interface(maas, cluster, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            base=dict(required=False),
+            cluster_name=dict(required=True),
+            name=dict(required=True),
+            interface=dict(required=False),
+            ip=dict(required=False),
+            subnet_mask=dict(required=False),
+            management=dict(default='unmanaged', choices=['unmanaged', 'dhcp', 'dhcpdns']),
+            ip_range_low=dict(required=False),
+            ip_range_high=dict(required=False),
+            static_ip_range_low=dict(required=False),
+            static_ip_range_high=dict(required=False),
+            broadcast_ip=dict(required=False),
+            router_ip=dict(required=False),
+            state=dict(default='present', choices=['present', 'absent', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    management_map = {
+        'unmanaged': 0,
+        'dhcp': 1,
+        'dhcpdns': 2
+    }
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'name': module.params['name'],
+        'interface': module.params['interface'],
+        'ip': module.params['ip'],
+        'subnet_mask': module.params['subnet_mask'],
+        'management': management_map[module.params['management']],
+        'ip_range_low': module.params['ip_range_low'],
+        'ip_range_high': module.params['ip_range_high'],
+        'static_ip_range_low': module.params['static_ip_range_low'],
+        'static_ip_range_high': module.params['static_ip_range_high'],
+        'broadcast_ip': module.params['broadcast_ip'],
+        'router_ip': module.params['router_ip'],
+    })
+
+    debug = []
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to locate the cluster on which we will be working, error out if it can't be found
+    cluster = get_cluster(maas, module.params['cluster_name'])
+    if not cluster:
+        module.fail_json(msg='Unable to find specified cluster "%s", cannot continue' % module.params['cluster_name'])
+        return
+
+    debug.append({"desired": desired})
+
+    # Attempt to get the cluster interface from MAAS
+    cluster_interface = get_cluster_interface(maas, cluster, desired['name'])
+
+    debug.append({"found": cluster_interface})
+
+    # Actions if the cluster interface does not currently exist
+    if not cluster_interface:
+        if state == 'query':
+            # If this is a query, returne it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # If this should be present, then attempt to create it
+            res = create_cluster_interface(maas, cluster, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, cluster_interface=res['status'], debug=debug)
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with cluster interfaces does not exists actions
+        return
+
+    # Actions if the cluster interface does exist
+    if state == 'query':
+        # If this is a query, return the cluster interface
+        module.exit_json(changed=False, found=True, cluster_interface=cluster_interface)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(cluster_interface, desired, debug):
+            res = update_cluster_interface(maas, cluster, cluster_interface, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, cluster_interface=res['status'], debug=debug)
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, cluster_interface=cluster_interface)
+    else:
+        # If we don't want this cluster interface, then delete it
+        res = delete_cluster_interface(maas, cluster, cluster_interface['name'])
+        if res['error']:
+            module.fail_json(msg=res['status'])
+        else:
+            module.exit_json(changed=True, cluster_interface=cluster_interface)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_item.py b/library/maas_item.py
new file mode 100644
index 0000000..429c167
--- /dev/null
+++ b/library/maas_item.py
@@ -0,0 +1,190 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_item
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  name:
+    description:
+      - name of the item
+    required: yes
+  state:
+    description:
+      - possible states for this item
+    choices: ['present', 'absent', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_item:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyItem
+    state: present
+
+  maas_item:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyDeadItem
+    state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            return True
+    return False
+
+# Get an item from MAAS using its name, if not found return None
+def get_item(maas, name):
+    res = maas.get('/items/%s/' % name)
+    if res.ok:
+        return json.loads(res.text)
+    return None
+
+# Create an item based on the value given
+def create_item(maas, item):
+    merged = item.copy()
+    # merged['op'] = 'new'
+    res = maas.post('/items/', merged)
+    if res.ok:
+        return { 'error': False, 'status': get_item(maas, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an item based on the name
+def delete_item(maas, name):
+    res = maas.delete('/items/%s/' % name)
+    if res.ok:
+        return { 'error': False }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_item(maas, have, want):
+    merged = have.copy()
+    merged.update(want)
+    res = put(maas, '/items/%s/' % merged['name'], merged)
+    if res.ok:
+        return { 'error': False, 'status': get_item(maas, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            name=dict(required=True),
+            state=dict(default='present', choices=['present', 'absent', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'name': module.params['name'],
+    })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the item from MAAS
+    item = get_item(maas, desired['name'])
+
+    # Actions if the item does not currently exist
+    if not item:
+        if state == 'query':
+            # If this is a query, returne it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # If this should be present, then attempt to create it
+            res = create_item(maas, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, item=res['status'])
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with items does not exists actions
+        return
+
+    # Actions if the item does exist
+    if state == 'query':
+        # If this is a query, return the item
+        module.exit_json(changed=False, found=True, item=item)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(item, desired):
+            res = update_item(maas, item, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, item=res['status'])
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, item=item)
+    else:
+        # If we don't want this item, then delete it
+        res = delete_item(maas, item['name'])
+        if res['error']:
+            module.fail_json(msg=res['status'])
+        else:
+            module.exit_json(changed=True, item=item)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_sshkey.py b/library/maas_sshkey.py
new file mode 100644
index 0000000..b901d4e
--- /dev/null
+++ b/library/maas_sshkey.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_sshkey
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  sshkey:
+    description:
+      - sshkey on which to operate
+    required: yes
+  state:
+    description:
+      - possible states for this sshkey
+    choices: ['present', 'absent', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_sshkey:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    sshkey: ... foo@company.com
+    state: present
+
+  maas_sshkeys:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    sshkey: ... foo@company.com
+    state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+def filter(filter_type, d, keys):
+    if filter_type == 'include':
+        for k in d.keys():
+            if k not in keys:
+                d.pop(k, None)
+    else:
+        for k in d.keys():
+            if k in keys:
+                d.pop(k, None)
+
+# Get an item from MAAS using its name, if not found return None
+def get_sshkey(maas, name):
+    res = maas.get('/account/prefs/sshkeys/', dict(op='list'))
+    if res.ok:
+        for sshkey in json.loads(res.text):
+            if sshkey['key'] == name:
+                return sshkey
+    return None
+
+# Create an item based on the value given
+def create_sshkey(maas, sshkey):
+    merged = sshkey.copy()
+    filter('include', merged, ['key'])
+    merged['op'] = 'new'
+    res = maas.post('/account/prefs/sshkeys/', merged)
+    if res.ok:
+        return { 'error': False, 'status': get_sshkey(maas, merged['key']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an item based on the name
+def delete_sshkey(maas, id):
+    res = maas.delete('/account/prefs/sshkeys/%s/' % id)
+    if res.ok:
+        return { 'error': False }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            sshkey=dict(required=True),
+            state=dict(default='present', choices=['present', 'absent', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'key': module.params['sshkey'],
+    })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the item from MAAS
+    sshkey = get_sshkey(maas, desired['key'])
+
+    # Actions if the item does not currently exist
+    if not sshkey:
+        if state == 'query':
+            # If this is a query, return it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # If this should be present, then attempt to create it
+            res = create_sshkey(maas, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, sshkey=res['status'])
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with items does not exists actions
+        return
+
+    # Actions if the item does exist
+    if state == 'query':
+        # If this is a query, return the sshkey
+        module.exit_json(changed=False, found=True, sshkey=sshkey)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        # No differences, to nothing to change
+        module.exit_json(changed=False, sshkey=sshkey)
+    else:
+        # If we don't want this item, then delete it
+        res = delete_sshkey(maas, item['id'])
+        if res['error']:
+            module.fail_json(msg=res['status'])
+        else:
+            module.exit_json(changed=True, sshkey=sshkey)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_subnet.py b/library/maas_subnet.py
new file mode 100644
index 0000000..1bc461f
--- /dev/null
+++ b/library/maas_subnet.py
@@ -0,0 +1,276 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_subnet
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  name:
+    description:
+      - name of the subnet
+    required: yes
+  space:
+    description:
+      - network space of the subnet
+  dns_servers:
+    description:
+      - dns servers for the subnet
+  gateway_ip:
+    description:
+      - gateway IP for the subnet
+  cidr:
+    description:
+      - cidr for the subnet
+  state:
+    description:
+      - possible states for this subnet
+    choices: ['present', 'absent', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_subnet:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MySubnet
+    state: present
+
+  maas_subnet:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyDeadSubnet
+    state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+import string
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+debug = []
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Removes keys from a dictionary either using an include or
+# exclude filter This change happens on given dictionary is
+# modified.
+def filter(filter_type, d, keys):
+    if filter_type == 'include':
+        for k in d.keys():
+            if k not in keys:
+                d.pop(k, None)
+    else:
+        for k in d.keys():
+            if k in keys:
+                d.pop(k, None)
+
+# Converts a subnet structure with names for the vlan and space to their
+# ID equivalents that can be used in a REST call to MAAS
+def convert(maas, subnet):
+    copy = subnet.copy()
+    copy['space'] = get_space(maas, subnet['space'])['id']
+    fabric_name, vlan_name = string.split(subnet['vlan'], ':', 1)
+    fabric = get_fabric(maas, fabric_name)
+    copy['vlan'] = get_vlan(maas, fabric, vlan_name)['id']
+    return copy
+
+# replaces the expanded VLAN object with a unique identifier of
+# `fabric`:`name`
+def simplify(subnet):
+    copy = subnet.copy()
+    if 'dns_servers' in copy.keys() and type(copy['dns_servers']) == list:
+        copy['dns_servers'] = ",".join(copy['dns_servers'])
+    if subnet['vlan'] and type(subnet['vlan']) == dict:
+        copy['vlan'] = "%s:%s" % (subnet['vlan']['fabric'], subnet['vlan']['name'])
+    return copy
+
+# Deterine if two dictionaries are different
+def different(have, want):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            debug.append({"have": have, "want": want, "key": key})
+            return True
+    return False
+
+# Get a space object form MAAS based on its name
+def get_space(maas, name):
+    res = maas.get('/spaces/')
+    if res.ok:
+        for space in json.loads(res.text):
+            if space['name'] == name:
+                return space
+    return None
+
+# Get a fabric object from MAAS based on its name
+def get_fabric(maas, name):
+    res = maas.get('/fabrics/')
+    if res.ok:
+        for fabric in json.loads(res.text):
+            if fabric['name'] == name:
+                return fabric
+    return None
+
+# Get a VLAN object form MAAS based on its name
+def get_vlan(maas, fabric, name ):
+    res = maas.get('/fabrics/%d/vlans/' % fabric['id'])
+    if res.ok:
+        for vlan in json.loads(res.text):
+            if vlan['name'] == name:
+                return vlan
+    return None
+
+# Get an subnet from MAAS using its name, if not found return None
+def get_subnet(maas, name):
+    res = maas.get('/subnets/')
+    if res.ok:
+        for subnet in json.loads(res.text):
+            if subnet['name'] == name:
+                return simplify(subnet)
+    return None
+
+# Create an subnet based on the value given
+def create_subnet(maas, subnet):
+    merged = subnet.copy()
+    # merged['op'] = 'new'
+    res = maas.post('/subnets/', convert(maas, merged))
+    if res.ok:
+        return { 'error': False, 'status': get_subnet(maas, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an subnet based on the name
+def delete_subnet(maas, name):
+    res = maas.delete('/subnets/%s/' % name)
+    if res.ok:
+        return { 'error': False }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_subnet(maas, have, want):
+    merged = have.copy()
+    merged.update(want)
+    res = put(maas, '/subnets/%s/' % merged['id'], convert(maas, merged))
+    if res.ok:
+        return { 'error': False, 'status': get_subnet(maas, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            name=dict(required=True),
+            space=dict(required=False),
+            dns_servers=dict(required=False),
+            gateway_ip=dict(required=False),
+            cidr=dict(required=False),
+            state=dict(default='present', choices=['present', 'absent', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'name': module.params['name'],
+        'space': module.params['space'],
+        'dns_servers': module.params['dns_servers'],
+        'gateway_ip': module.params['gateway_ip'],
+        'cidr': module.params['cidr'],
+    })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the subnet from MAAS
+    subnet = get_subnet(maas, desired['name'])
+
+    # Actions if the subnet does not currently exist
+    if not subnet:
+        if state == 'query':
+            # If this is a query, returne it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # If this should be present, then attempt to create it
+            res = create_subnet(maas, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, subnet=res['status'])
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with subnets does not exists actions
+        return
+
+    # Actions if the subnet does exist
+    if state == 'query':
+        # If this is a query, return the subnet
+        module.exit_json(changed=False, found=True, subnet=subnet)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(subnet, desired):
+            res = update_subnet(maas, subnet, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, subnet=res['status'], debug=debug)
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, subnet=subnet)
+    else:
+        # If we don't want this subnet, then delete it
+        res = delete_subnet(maas, subnet['name'])
+        if res['error']:
+            module.fail_json(msg=res['status'])
+        else:
+            module.exit_json(changed=True, subnet=subnet)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_user.py b/library/maas_user.py
new file mode 100644
index 0000000..285269f
--- /dev/null
+++ b/library/maas_user.py
@@ -0,0 +1,200 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_user
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  name:
+    description:
+      - name of the user
+    required: yes
+  email:
+    description:
+      - email address of the user
+    required: no
+  password:
+    description:
+      - password for the user
+    required: no
+  is_superuser:
+    description:
+      - does the user have priviledges
+    default: no
+  state:
+    description:
+      - possible states for this user
+    choices: ['present', 'absent', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_user:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyUser
+    email: user@company.com
+    password: donttell
+    is_superuser: no
+    state: present
+
+  maas_user:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyDeadUser
+    state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            return True
+    return False
+
+# Get an user from MAAS using its name, if not found return None
+def get_user(maas, name):
+    res = maas.get('/users/%s/' % name)
+    if res.ok:
+        return json.loads(res.text)
+    return None
+
+# Create an user based on the value given
+def create_user(maas, user):
+    merged = user.copy()
+    # merged['op'] = 'new'
+    res = maas.post('/users/', merged)
+    if res.ok:
+        return { 'error': False, 'status': get_user(maas, merged['username']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an user based on the name
+def delete_user(maas, name):
+    res = maas.delete('/users/%s/' % name)
+    if res.ok:
+        return { 'error': False }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            name=dict(required=True),
+            email=dict(required=False),
+            password=dict(required=False),
+            is_superuser=dict(default=False, type='bool'),
+            state=dict(default='present', choices=['present', 'absent', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'username': module.params['name'],
+        'email': module.params['email'],
+        'password': module.params['password'],
+        'is_superuser': 0 if not module.params['is_superuser'] else 1
+    })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the user from MAAS
+    user = get_user(maas, desired['username'])
+
+    # Actions if the user does not currently exist
+    if not user:
+        if state == 'query':
+            # If this is a query, returne it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # If this should be present, then attempt to create it
+            res = create_user(maas, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, user=res['status'])
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with users does not exists actions
+        return
+
+    # Actions if the user does exist
+    if state == 'query':
+        # If this is a query, return the user
+        module.exit_json(changed=False, found=True, user=user)
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(user, desired):
+            module.fail_json(msg='Specified user, "%s", exists and MAAS does not allow the user to be modified programatically'
+                    % user['username'])
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, user=user)
+    else:
+        # If we don't want this user, then delete it
+        res = delete_user(maas, user['username'])
+        if res['error']:
+            module.fail_json(msg=res['status'])
+        else:
+            module.exit_json(changed=True, user=user)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/maas_zone.py b/library/maas_zone.py
new file mode 100644
index 0000000..bf8d684
--- /dev/null
+++ b/library/maas_zone.py
@@ -0,0 +1,199 @@
+#!/usr/bin/python
+
+DOCUMENTATION = '''
+---
+module: maas_zone
+short_description: Manage MAAS Clusters Interfaces
+options:
+  maas:
+    description:
+      - URL of MAAS server
+    default: http://localhost/MAAS/api/1.0/
+  key:
+    description:
+      - MAAS API key
+    required: yes
+  name:
+    description:
+      - name of the zone
+    required: yes
+  description:
+    description:
+      - description text of zone
+    required: no
+  state:
+    description:
+      - possible states for this zone
+    choices: ['present', 'absent', 'query']
+    default: present
+
+requirements: [ipaddress, requests_oauthlib, maasclient]
+author: David Bainbridge
+'''
+
+EXAMPLES = '''
+examples:
+  maas_zone:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyZone
+    description: This is my zone
+    state: present
+
+  maas_zone:
+    maas: http://my.maas.server.com/MAAS/api/1.0/
+    key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
+    name: MyDeadZone
+    description: This was my zone
+    state: absent
+'''
+
+import sys
+import json
+import ipaddress
+import requests
+from maasclient.auth import MaasAuth
+from maasclient import MaasClient
+
+# For some reason the maasclient doesn't provide a put method. So
+# we will add it here
+def put(client, url, params=None):
+    return requests.put(url=client.auth.api_url + url,
+                        auth=client._oauth(), data=params)
+
+# Attempt to interpret the given value as a JSON object, if that fails
+# just return it as a string
+def string_or_object(val):
+    try:
+        return json.loads(val)
+    except:
+        return val
+
+# Return a copy of the given dictionary with any `null` valued entries
+# removed
+def remove_null(d_in):
+    d = d_in.copy()
+    to_remove = []
+    for k in d.keys():
+        if d[k] == None:
+            to_remove.append(k)
+    for k in to_remove:
+        del d[k]
+    return d
+
+# Deterine if two dictionaries are different
+def different(have, want):
+    have_keys = have.keys()
+    for key in want.keys():
+        if (key in have_keys and want[key] != have[key]) or key not in have_keys:
+            return True
+    return False
+
+# Get an zone from MAAS using its name, if not found return None
+def get_zone(maas, name):
+    res = maas.get('/zones/%s/' % name)
+    if res.ok:
+        return json.loads(res.text)
+    return None
+
+# Create an zone based on the value given
+def create_zone(maas, zone):
+    merged = zone.copy()
+    # merged['op'] = 'new'
+    res = maas.post('/zones/', merged)
+    if res.ok:
+        return { 'error': False, 'status': get_zone(maas, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+# Delete an zone based on the name
+def delete_zone(maas, name):
+    res = maas.delete('/zones/%s/' % name)
+    if res.ok:
+        return { 'error': False }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def update_zone(maas, have, want):
+    merged = have.copy()
+    merged.update(want)
+    res = put(maas, '/zones/%s/' % merged['name'], merged)
+    if res.ok:
+        return { 'error': False, 'status': get_zone(maas, merged['name']) }
+    return { 'error': True, 'status': string_or_object(res.text) }
+
+def main():
+    module = AnsibleModule(
+        argument_spec = dict(
+            maas=dict(default='http://localhost/MAAS/api/1.0/'),
+            key=dict(required=True),
+            name=dict(required=True),
+            description=dict(required=False),
+            state=dict(default='present', choices=['present', 'absent', 'query'])
+        ),
+        supports_check_mode = False
+    )
+
+    maas = module.params['maas']
+    key = module.params['key']
+    state = module.params['state']
+
+    # Construct a sparsely populate desired state
+    desired = remove_null({
+        'name': module.params['name'],
+        'description': module.params['description'],
+    })
+
+    # Authenticate into MAAS
+    auth = MaasAuth(maas, key)
+    maas = MaasClient(auth)
+
+    # Attempt to get the zone from MAAS
+    zone = get_zone(maas, desired['name'])
+
+    # Actions if the zone does not currently exist
+    if not zone:
+        if state == 'query':
+            # If this is a query, returne it is not found
+            module.exit_json(changed=False, found=False)
+        elif state == 'present':
+            # If this should be present, then attempt to create it
+            res = create_zone(maas, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, zone=res['status'])
+        else:
+            # If this should be absent, then we are done and in the desired state
+            module.exit_json(changed=False)
+
+        # Done with zones does not exists actions
+        return
+
+    # Actions if the zone does exist
+    if state == 'query':
+        # If this is a query, return the zone
+        module.exit_json(changed=False, found=True, zone=zone)
+        return
+    elif state == 'present':
+        # If we want this to exists check to see if this is different and
+        # needs updated
+        if different(zone, desired):
+            res = update_zone(maas, zone, desired)
+            if res['error']:
+                module.fail_json(msg=res['status'])
+            else:
+                module.exit_json(changed=True, zone=res['status'])
+        else:
+            # No differences, to nothing to change
+            module.exit_json(changed=False, zone=zone)
+    else:
+        # If we don't want this zone, then delete it
+        res = delete_zone(maas, zone['name'])
+        if res['error']:
+            module.fail_json(msg=res['status'])
+        else:
+            module.exit_json(changed=True, zone=zone)
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+if __name__ == '__main__':
+    main()
diff --git a/library/netfile.py b/library/netfile.py
index 86868bf..ce0b474 100755
--- a/library/netfile.py
+++ b/library/netfile.py
@@ -211,7 +211,7 @@
 if name == "":
     result = {
         "changed": False,
-	"failed": True,
+        "failed": True,
         "msg": "Name is a mansitory parameter",
     }
     print json.dumps(result)
diff --git a/roles/compute-node/files/delete-fabric-config b/roles/compute-node/files/delete-fabric-config
index 4ddce2d..fbe2f5c 100755
--- a/roles/compute-node/files/delete-fabric-config
+++ b/roles/compute-node/files/delete-fabric-config
@@ -2,4 +2,4 @@
 
 CFG=${1:-"config-tibit.json"}
 
-curl -v -XDELETE -H Content-Type: application/json http://karaf:karaf@onos-fabric:8181/onos/v1/network/configuration 
+curl -v -XDELETE -H Content-Type: application/json http://karaf:karaf@onos-fabric:8181/onos/v1/network/configuration
diff --git a/roles/head-node/files/commands/cord b/roles/head-node/files/commands/cord
index 0d63dc8..9f675d5 100755
--- a/roles/head-node/files/commands/cord
+++ b/roles/head-node/files/commands/cord
@@ -16,7 +16,7 @@
     for CMD in $ALL_FILES; do
         test -x "$CMD" && COMMANDS="$COMMANDS $CMD"
     done
-       
+
     # Process comands for usage information
     # Output all commands and their help information to file
     # so it can be sorted. The format will be:
diff --git a/roles/head-node/files/commands/cord-harvest b/roles/head-node/files/commands/cord-harvest
index fd43182..8cabd11 100755
--- a/roles/head-node/files/commands/cord-harvest
+++ b/roles/head-node/files/commands/cord-harvest
@@ -50,7 +50,7 @@
         fi
         ;;
     check)
-        RUNNING=$($SSH_OPT docker inspect --format="'{{ .State.Running }}'" harvester) 
+        RUNNING=$($SSH_OPT docker inspect --format="'{{ .State.Running }}'" harvester)
 	if [ $? -ne 0 ]; then
 	    >&2 echo "Unable to execute docker or locate harvester container, if not running on the head node please specify the server address"
 	    exit 1
@@ -112,4 +112,4 @@
         exit 1
         ;;
 esac
-  
+
diff --git a/roles/maas/tasks/config-maas.yml b/roles/maas/tasks/config-maas.yml
new file mode 100644
index 0000000..d7e0d1c
--- /dev/null
+++ b/roles/maas/tasks/config-maas.yml
@@ -0,0 +1,74 @@
+---
+- name: Ensure PIP
+  become: yes
+  apt:
+    name: python-pip
+    state: present
+
+- name: Ensure Prerequisites
+  become: yes
+  pip:
+    name: "{{item}}"
+    state: present
+  with_items:
+    - maasclient==0.3
+    - requests_oauthlib
+    - ipaddress
+
+- name: Ensure SSH Key
+  maas_sshkey:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    sshkey: '{{maas.user_sshkey}}'
+    state: present
+
+- name: Ensure Administrative Zone
+  maas_zone:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    name: administrative
+    state: present
+
+- name: Ensure Cluster
+  maas_cluster:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    name: 'Cluster master'
+    status: enabled
+    domain: '{{maas.domain}}'
+
+- name: Ensure Management Interface
+  maas_cluster_interface:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    cluster_name: 'Cluster master'
+    name: '{{interfaces.management}}'
+    interface: '{{interfaces.management}}'
+    management: dhcpdns
+    ip: "{{networks.management | ipaddr(1) | ipaddr('address')}}"
+    subnet_mask: "{{networks.management | ipaddr('netmask')}}"
+    broadcast_ip: "{{networks.management | ipaddr('broadcast')}}"
+    ip_range_low: '{{ranges.management.low}}'
+    ip_range_high: '{{ranges.management.high}}'
+    state: present
+
+- name: Ensure Subnet Configuration
+  maas_subnet:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    name: '{{networks.management}}'
+    gateway_ip: "{{networks.management | ipaddr(1) | ipaddr('address')}}"
+    dns_servers: "{{networks.management | ipaddr(1) | ipaddr('address')}}"
+
+- name: Ensure Upstream DNS Server
+  maas:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    upstream_dns: '{{maas.upstream_dns}}'
+    state: present
+
+- name: Ensure Boot Resources
+  maas_boot_resource:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    state: import
diff --git a/roles/maas/tasks/main.yml b/roles/maas/tasks/main.yml
index fd136b1..98ac070 100644
--- a/roles/maas/tasks/main.yml
+++ b/roles/maas/tasks/main.yml
@@ -182,18 +182,8 @@
   tags:
     - maas_restart
 
-#- name: Ensure latest bootstrap image
-#  become: yes
-#  docker_image:
-#    name: localhost: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}}' --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"
+- name: Ensure MAAS Configuration
+  include: config-maas.yml
 
 - name: Custom MAAS Configuration Template
   become: yes
@@ -264,9 +254,10 @@
     - maas_restart
 
 - name: Ensure Boot Resource Import Started
-  become: yes
-  shell: maas login cord http://localhost/MAAS/api/1.0/ "{{apikey.stdout}}" && maas cord boot-resources import && maas logout cord
-  changed_when: true
+  maas_boot_resource:
+    key: '{{apikey.stdout}}'
+    maas: 'http://{{mgmt_ip_address.stdout}}/MAAS/api/1.0'
+    state: import
 
 - name: Ensure VirtualBox Power Management
   include: virtualbox.yml
diff --git a/roles/maas/vars/main.yml b/roles/maas/vars/main.yml
index 59bee09..cb3a397 100644
--- a/roles/maas/vars/main.yml
+++ b/roles/maas/vars/main.yml
@@ -21,6 +21,7 @@
     # CHANGE:
     #   'domain' specifies the domain name configured in to MAAS
     domain: "{{ domain | default('cord.lab') }}"
+    upstream_dns: "{{ upstream_dns | default('8.8.8.8 8.8.8.4') }}"
 
 interfaces:
     # CHANGE:
diff --git a/roles/onos-fabric/files/bin/ping-test.sh b/roles/onos-fabric/files/bin/ping-test.sh
index d7b894e..4bbcef9 100755
--- a/roles/onos-fabric/files/bin/ping-test.sh
+++ b/roles/onos-fabric/files/bin/ping-test.sh
@@ -2,7 +2,7 @@
 
 HOSTS="10.3.1.1 10.3.1.2 10.3.2.1 10.3.2.2 10.3.1.254 10.3.2.254 192.168.10.1 8.8.8.8"
 
-ME=$(ifconfig | grep "10\.3\.[0-9]\.[0-9]" | sed -e 's/.*addr:\(10\.3\.[0-9]\.[0-9]\).*/\1/g' 2> /dev/null) 
+ME=$(ifconfig | grep "10\.3\.[0-9]\.[0-9]" | sed -e 's/.*addr:\(10\.3\.[0-9]\.[0-9]\).*/\1/g' 2> /dev/null)
 echo "FROM: $ME"
 for TO in $HOSTS; do
     T=$(ping -q -c 1 -W 1 -I eth0 $TO | grep rtt | awk '{print $4}' | sed -e 's|/| |g') #sed -e 's|r| |')
diff --git a/roles/onos-fabric/templates/fabric-network-config.json.j2 b/roles/onos-fabric/templates/fabric-network-config.json.j2
index f39cf60..f29134c 100644
--- a/roles/onos-fabric/templates/fabric-network-config.json.j2
+++ b/roles/onos-fabric/templates/fabric-network-config.json.j2
@@ -212,7 +212,7 @@
         "org.onosproject.core" : {
             "core" : {
                 "linkDiscoveryMode" : "STRICT" // enable strict link validation
-            }    
+            }
         }
     }
 }