Extending functionality of maasng:

* machine_power_state: Check power state of a node
* list_ipaddresses: get list of reserved IPs
* reserve_ipaddress: reserve ip address in specific subnet
* release_ipaddress: release specified ip address
* list_dnsresources: get list of dns records from maas
* sync_address_pool: sync address pool from pillar to maas

  Example:

    openstack_share_node02_deploy_address: deploy_network

  would be recognized as an ip address request from deploy_network
  maasng.reserve_ipaddress openstack_share_node02_deploy_address \
      deploy_network["cidr"]

  will happen.
  Maas reservation from CIDR would be used in ext_pillar to
  back populate and overwrite 'deploy_network' with an ip address.

    salt '*' pillar.get openstack_share_node02_deploy_address

  will return IP address instead of 'deploy_network'

Change-Id: Idac2849a82e30df683df2a83824544ca5f0265f2
diff --git a/README.rst b/README.rst
index 20da43e..6304634 100644
--- a/README.rst
+++ b/README.rst
@@ -594,6 +594,154 @@
               os_password: password
               os_authurl: http://url
 
+
+Ext pillar from MAAS address pool:
+==================================
+
+Set up salt master:
+
+.. code-block:: yaml
+
+    salt:
+      master:
+        ext_pillars:
+          1:
+            module: cmd_json
+            params: /usr/share/salt-formulas/env/_modules/maas-IPAM.py --address_pool ${salt:master:pillar:data_dir}/classes/cluster/${_param:cluster_name}/infra/address_pool.yml
+
+.. code-block:: bash
+
+    salt-call state.apply salt.master
+    salt '*' saltutil.refresh_pillar
+
+Update infra/address_pool.yml:
+
+.. code-block:: yaml
+
+    parameters:
+      address_pool:
+        external:
+          dns_server01: 8.8.8.8
+          dns_server02: 8.8.4.4
+          upstream_ntp_server: 193.27.208.100
+          remote_rsyslog_host: 127.0.0.3
+        deploy_network:
+          address: 192.168.0.0
+          netmask: 255.255.255.0
+          gateway: 192.168.0.1
+          prefix: 24
+          vlan: 0
+          # Static reservation which interfere with maas reserve pool
+          reserved:
+            cmp001_deploy_address: 192.168.0.101
+            cmp002_deploy_address: 192.168.0.102
+            infra_config_deploy_address: 192.168.0.253
+            infra_kvm_node01_deploy_address: 192.168.0.241
+            infra_kvm_node02_deploy_address: 192.168.0.242
+            infra_kvm_node03_deploy_address: 192.168.0.243
+            infra_kvm_node04_deploy_address: 192.168.0.244
+            infra_kvm_node05_deploy_address: 192.168.0.245
+            infra_kvm_node06_deploy_address: 192.168.0.246
+            ldap_ip_address: 192.168.0.249
+          pool:
+            # Static reservation out of maas reserved pool
+            aptly_server_deploy_address: 192.168.0.252
+            # Dynamic serialization
+            cicd_control_node01_deploy_address: dummy
+            cicd_control_node02_deploy_address: dummy
+            cicd_control_node03_deploy_address: dummy
+            # Release IP address
+            openstack_share_node02_proxy_address: ""
+      cluster_networks:
+        deploy_network:
+          name: 'deploy_network'
+          cidr: ${address_pool:deploy_network:address}/${address_pool:deploy_network:prefix}
+          fabric: deploy_fabric
+          vlan: ${address_pool:deploy_network:vlan}
+          gateway_ip: ${address_pool:deploy_network:gateway}
+          ipranges:
+            1:
+              start: 192.168.0.30
+              end: 192.168.0.80
+              type: dynamic
+              comment: 'dynamic range'
+            2:
+              start: 192.168.0.1
+              end: 192.168.0.29
+              type: reserved
+              comment: 'infra reserve'
+        control_network:
+          name: 'control_network'
+          cidr: ${address_pool:control_network:address}/${address_pool:control_network:prefix}
+          fabric: control_fabric
+          vlan: ${address_pool:control_network:vlan}
+          gateway_ip: ${address_pool:control_network:address}
+
+
+Update maas.yml:
+
+.. code-block:: yaml
+
+      maas:
+        region:
+          fabrics:
+            deploy_fabric:
+              name: ${cluster_networks:deploy_network:fabric}
+              description: 'Fabric for deploy_network'
+              vlans:
+                0:
+                  name: 'lan 0'
+                  description: Deploy VLAN
+                  dhcp: true
+                  primary_rack: "${linux:network:hostname}"
+            control_fabric:
+              name: 'control_fabric'
+              description: 'Fabric for control_network'
+              vlans:
+                0:
+                  name: ${cluster_networks:control_network:fabric}
+                  description: Control VLAN
+                  dhcp: false
+                  primary_rack: "${linux:network:hostname}"
+            mesh_fabric:
+              name: ${cluster_networks:mesh_network:fabric}
+              description: 'Fabric for mesh_network'
+              vlans:
+                0:
+                  name: 'mesh_network'
+                  description: Mesh VLAN
+                  dhcp: false
+                  primary_rack: "${linux:network:hostname}"
+          subnets:
+            deploy_network: ${cluster_networks:deploy_network}
+            control_network: ${cluster_networks:control_network}
+            mesh_network: ${cluster_networks:mesh_network}
+            proxy_network: ${cluster_networks:proxy_network}
+
+
+Populate maas with networks:
+
+.. code-block:: bash
+
+    salt-call state.apply maas.region
+
+Serialize ip addresses using maas network pools:
+
+.. code-block:: bash
+
+    salt-call maasng.sync_address_pool
+
+Verify pillar override works:
+
+.. code-block:: bash
+
+    salt-call pillar.get address_pool:deploy_network:pool:openstack_share_node02_deploy_address
+
+    # Sample output:
+    # local:
+    #     192.168.0.81
+
+
 Test pillars
 ==============
 
diff --git a/_modules/maas-IPAM.py b/_modules/maas-IPAM.py
new file mode 100755
index 0000000..3e910b8
--- /dev/null
+++ b/_modules/maas-IPAM.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python2.7
+from argparse import ArgumentParser
+import json
+import yaml
+import logging
+
+from maas_client import MAASClient, MAASDispatcher, MAASOAuth
+from maasng import list_dnsresources
+
+parser = ArgumentParser()
+parser.add_argument('--address_pool', help='Path to address_pool \
+                    yaml file', required=True)
+parser.add_argument('--debug', action='store_true', default=False)
+
+def maas_IPAM():
+    args = parser.parse_args()
+
+    handler = logging.StreamHandler()
+
+    if args.debug:
+        log_level = logging.DEBUG
+    else:
+        log_level = logging.INFO
+
+    LOG = logging.getLogger()
+    LOG.setLevel(log_level)
+    LOG.addHandler(handler)
+
+    with open(args.address_pool, 'r') as f:
+        yaml_data = yaml.safe_load(f)
+
+    # TODO: (dstremkouski)
+    # schema validator for address pool
+    address_pool = yaml_data["parameters"]["address_pool"]
+    dnsresources = list_dnsresources()
+
+    for dnsres in dnsresources:
+        mapping_found = False
+        for net in address_pool:
+            if net == 'external':
+                continue
+            if mapping_found:
+                continue
+            for addr in address_pool[net]['pool']:
+                if dnsres["hostname"] == addr:
+                    address_pool[net]['pool'][addr] = dnsres["ip_addresses"][0]
+                    mapping_found = True
+                    break
+
+    return('{"address_pool": ' + json.dumps(address_pool) + '}')
+
+if __name__ == "__main__":
+    print maas_IPAM()
diff --git a/_modules/maasng.py b/_modules/maasng.py
index 0ec08f6..29a2c07 100644
--- a/_modules/maasng.py
+++ b/_modules/maasng.py
@@ -21,6 +21,7 @@
 import logging
 import time
 import urllib2
+import netaddr
 # Salt utils
 from salt.exceptions import CommandExecutionError, SaltInvocationError
 
@@ -109,6 +110,39 @@
     # TODO validation
     return list_partitions(hostname, device)[partition]["id"]
 
+def is_valid_ipv4(address):
+    """Verify that address represents a valid IPv4 address.
+    :param address: Value to verify
+    :type address: string
+    :returns: bool
+    .. versionadded:: 1.1
+    """
+    try:
+        return netaddr.valid_ipv4(address)
+    except netaddr.AddrFormatError:
+        return False
+
+def is_valid_ipv6(address):
+    """Verify that address represents a valid IPv6 address.
+    :param address: Value to verify
+    :type address: string
+    :returns: bool
+    .. versionadded:: 1.1
+    """
+    if not address:
+        return False
+
+    parts = address.rsplit("%", 1)
+    address = parts[0]
+    scope = parts[1] if len(parts) > 1 else None
+    if scope is not None and (len(scope) < 1 or len(scope) > 15):
+        return False
+
+    try:
+        return netaddr.valid_ipv6(address, netaddr.core.INET_PTON)
+    except netaddr.AddrFormatError:
+        return False
+
 # MACHINE SECTION
 
 
@@ -185,6 +219,31 @@
     result["new"] = "Machine {0} deleted".format(hostname)
     return result
 
+def machine_power_state(hostname):
+    """
+    Query the power state of a node.
+
+    :param hostname: Node hostname
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt 'maas-node' maasng.machine_power_state kvm06
+
+    """
+    result = {}
+    maas = _create_maas_client()
+    system_id = get_machine(hostname)["system_id"]
+    LOG.debug('action_machine: {}'.format(system_id))
+
+    # TODO validation
+    json_res = json.loads(maas.get(
+        u"api/2.0/machines/{0}/".format(system_id), "query_power_state").read())
+    LOG.info(json_res)
+
+    return json_res
+
 def action_machine(hostname, action, comment=None):
     """
     Send simple action (e.g. mark_broken, mark_fixed) to machine.
@@ -496,6 +555,220 @@
     return result
 
 
+def list_dnsresources():
+    """
+    List DNS resources known to MAAS.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt-call maasng.list_dnsresources
+
+    """
+    result = {}
+    res_json = []
+    maas = _create_maas_client()
+
+    # TODO validation
+    result = json.loads(maas.get(u"api/2.0/dnsresources/").read())
+    for elem in result:
+        ip_addresses = []
+        for ip in elem["ip_addresses"]:
+            ip_addresses.append(ip["ip"])
+        res_json.append(
+            {
+                "ip_addresses": ip_addresses,
+                "id": elem["id"],
+                "fqdn": elem["fqdn"],
+                "hostname": elem["fqdn"].split(".")[0]
+            }
+        )
+
+    LOG.debug(res_json)
+
+    return res_json
+
+
+def list_ipaddresses():
+    """
+    List IP addresses known to MAAS.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt-call maasng.list_ipaddresses
+
+    """
+    result = {}
+    res_json = []
+    maas = _create_maas_client()
+
+    # TODO validation
+    result = json.loads(maas.get(u"api/2.0/ipaddresses/?all").read())
+    for elem in result:
+        res_json.append(
+            {
+                "ip": elem["ip"],
+                "owner": { "username": elem["owner"]["username"] },
+                "created": elem["created"],
+                "alloc_type_name": elem["alloc_type_name"],
+                "alloc_type": elem["alloc_type"],
+                "subnet": {
+                    "id": elem["subnet"]["id"],
+                    "cidr": elem["subnet"]["cidr"],
+                    "name": elem["subnet"]["name"]
+                }
+            }
+        )
+
+    LOG.debug(res_json)
+
+    return res_json
+
+
+def reserve_ipaddress(hostname,subnet,ip=""):
+    """
+    Reserve IP address for specified hostname in specified subnet
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt-call maasng.reserve_ipaddress hostname 192.168.0.0/24 192.168.0.254
+        salt-call maasng.reserve_ipaddress hostname 192.168.0.0/24
+
+    """
+    result = {}
+    data = {}
+    maas = _create_maas_client()
+
+    data = {
+        "subnet": subnet,
+        "hostname": hostname
+    }
+
+    if ip:
+        data["ip"] = ip
+
+    # TODO validation
+    result = json.loads(maas.post(u"api/2.0/ipaddresses/", "reserve", **data).read())
+    res_json = {
+                   "created": result["created"],
+                   "type": "DNS",
+                   "hostname": hostname,
+                   "ip": result["ip"]
+               }
+
+    LOG.info(res_json)
+
+    return res_json
+
+
+def release_ipaddress(ipaddress):
+    """
+    Release an IP address that was previously reserved by the user.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt-call maasng.release_ipaddress 192.168.2.10
+
+    """
+    result = {}
+    data = {}
+    maas = _create_maas_client()
+
+    data = {
+      "ip": ipaddress
+    }
+
+    # TODO validation
+    return maas.post(u"api/2.0/ipaddresses/", "release", **data).read()
+
+
+def sync_address_pool():
+    """
+    Manage address pool for ext_pillar.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt-call maasng.sync_address_pool
+
+    """
+
+    address_pool = __pillar__["address_pool"]
+    LOG.debug("Address pool:")
+    LOG.debug(address_pool)
+
+    cluster_networks = __pillar__["cluster_networks"]
+    LOG.debug("Cluster networks:")
+    LOG.debug(cluster_networks)
+
+    dnsresources = list_dnsresources()
+    LOG.debug("DNS resources:")
+    LOG.debug(dnsresources)
+
+    machines = list_machines()
+    LOG.debug("Machines:")
+    LOG.debug(machines)
+
+    for net in address_pool:
+        if net == "external":
+            continue
+        for addr in address_pool[net]['pool']:
+            ipaddr = address_pool[net]['pool'][addr]
+            if ipaddr == "":
+                LOG.debug('Releasing IP address for: ' + addr)
+                release_required = False
+                for elem in dnsresources:
+                    if elem["hostname"] == addr:
+                        release_required = True
+                        ip_addresses = elem["ip_addresses"]
+                if release_required:
+                    for ip in ip_addresses:
+                        res = release_ipaddress(ip)
+                        LOG.debug(res)
+                else:
+                    LOG.debug('IP for ' + addr + ' already released')
+            elif is_valid_ipv6(ipaddr) or is_valid_ipv4(ipaddr):
+                LOG.debug('Ensure static IP address "' + ipaddr + '" for ' + addr)
+                reserve_required = True
+                for elem in dnsresources:
+                    if elem["hostname"] == addr:
+                        reserve_required = False
+                for elem, elemval in machines.iteritems():
+                    for iface in elemval["interface_set"]:
+                        for link in iface["links"]:
+                            if "ip_address" in link:
+                                if link["ip_address"] == ipaddr:
+                                    reserve_required = False
+                if reserve_required:
+                    res = reserve_ipaddress(addr, cluster_networks[net]['cidr'], ipaddr)
+                    reserve_required = False
+                    LOG.debug(res)
+                else:
+                    LOG.debug('Static IP address "' + ipaddr + '" for ' + addr + ' ensured')
+            else:
+                LOG.debug('Requesting IP address for' + addr)
+                reserve_required = True
+                for elem in dnsresources:
+                    if elem["hostname"] == addr:
+                        reserve_required = False
+                        ip = elem["ip_addresses"][0]
+                if reserve_required:
+                    res = reserve_ipaddress(addr, cluster_networks[net]['cidr'])
+                    LOG.debug(res)
+                else:
+                    LOG.debug(addr + " already has IP " + ip)
+
+    return True
+
+
 def delete_partition(hostname, disk, partition_name):
     """
     Delete partition on device.