CORD-654 configure MAAS via ansible module

Change-Id: I9f1b3b83b41d2cf1e87660d4a50fd3267ad1679a
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)