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.