diff --git a/lib/ansible/module_utils/dimensiondata.py b/lib/ansible/module_utils/dimensiondata.py
new file mode 100644
index 0000000000..33d2fee2a6
--- /dev/null
+++ b/lib/ansible/module_utils/dimensiondata.py
@@ -0,0 +1,426 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016 Dimension Data
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see .
+#
+# Authors:
+# - Aimon Bustardo
+# - Mark Maglana
+# - Adam Friedman
+#
+# Common methods to be used by versious module components
+import os
+from ansible.module_utils.six.moves.configparser import ConfigParser
+from ansible.module_utils.pycompat24 import get_exception
+from os.path import expanduser
+from uuid import UUID
+
+try:
+ from libcloud.common.dimensiondata import \
+ API_ENDPOINTS, DimensionDataAPIException
+ HAS_LIBCLOUD = True
+except ImportError:
+ HAS_LIBCLOUD = False
+
+
+# Custom Exceptions
+
+class LibcloudNotFound(Exception):
+ pass
+
+
+class MissingCredentialsError(Exception):
+ pass
+
+
+class UnknownNetworkError(Exception):
+ pass
+
+
+class UnknownVLANError(Exception):
+ pass
+
+
+def check_libcloud_or_fail():
+ """
+ Checks if libcloud is installed and fails if not
+ """
+ if not HAS_LIBCLOUD:
+ raise LibcloudNotFound("apache-libcloud is required.")
+
+
+def get_credentials(module):
+ """
+ Get user_id and key from module configuration, environment, or dotfile.
+ Order of priority is module, environment, dotfile.
+
+ To set in environment:
+
+ export MCP_USER='myusername'
+ export MCP_PASSWORD='mypassword'
+
+ To set in dot file place a file at ~/.dimensiondata with
+ the following contents:
+
+ [dimensiondatacloud]
+ MCP_USER: myusername
+ MCP_PASSWORD: mypassword
+ """
+
+ if not HAS_LIBCLOUD:
+ module.fail_json(msg='libcloud is required for this module.')
+
+ return None
+
+ user_id = None
+ key = None
+
+ # First, try the module configuration
+ if 'mcp_user' in module.params:
+ if 'mcp_password' not in module.params:
+ module.fail_json(
+ '"mcp_user" parameter was specified, but not "mcp_password" ' +
+ '(either both must be specified, or neither).'
+ )
+
+ return None
+
+ user_id = module.params['mcp_user']
+ key = module.params['mcp_password']
+
+ # Fall back to environment
+ if not user_id or not key:
+ user_id = os.environ.get('MCP_USER', None)
+ key = os.environ.get('MCP_PASSWORD', None)
+
+ # Finally, try dotfile (~/.dimensiondata)
+ if not user_id or not key:
+ home = expanduser('~')
+ config = ConfigParser.RawConfigParser()
+ config.read("%s/.dimensiondata" % home)
+
+ try:
+ user_id = config.get("dimensiondatacloud", "MCP_USER")
+ key = config.get("dimensiondatacloud", "MCP_PASSWORD")
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ pass
+
+ # One or more credentials not found. Function can't recover from this
+ # so it has to raise an error instead of fail silently.
+ if not user_id:
+ raise MissingCredentialsError("Dimension Data user id not found")
+ elif not key:
+ raise MissingCredentialsError("Dimension Data key not found")
+
+ # Both found, return data
+ return dict(user_id=user_id, key=key)
+
+
+def get_dd_regions():
+ """
+ Get the list of available regions whose vendor is Dimension Data.
+ """
+ check_libcloud_or_fail()
+
+ # Get endpoints
+ all_regions = API_ENDPOINTS.keys()
+
+ # Only Dimension Data endpoints (no prefix)
+ regions = [region[3:] for region in all_regions if region.startswith('dd-')]
+
+ return regions
+
+
+def get_network_domain_by_name(driver, name, location):
+ """
+ Get a network domain object by its name
+ """
+ networks = driver.ex_list_network_domains(location=location)
+ found_networks = [network for network in networks if network.name == name]
+
+ if not found_networks:
+ raise UnknownNetworkError("Network '%s' could not be found" % name)
+
+ return found_networks[0]
+
+
+def get_network_domain(driver, locator, location):
+ """
+ Get a network domain object by its name or id
+ """
+ if is_uuid(locator):
+ net_id = locator
+ else:
+ name = locator
+ networks = driver.ex_list_network_domains(location=location)
+ found_networks = [network for network in networks if network.name == name]
+
+ if not found_networks:
+ raise UnknownNetworkError("Network '%s' could not be found" % name)
+
+ net_id = found_networks[0].id
+
+ return driver.ex_get_network_domain(net_id)
+
+
+def get_vlan(driver, locator, location, network_domain):
+ """
+ Get a VLAN object by its name or id
+ """
+ if is_uuid(locator):
+ vlan_id = locator
+ else:
+ vlans = driver.ex_list_vlans(location=location,
+ network_domain=network_domain)
+ found_vlans = [vlan for vlan in vlans if vlan.name == locator]
+
+ if not found_vlans:
+ raise UnknownVLANError("VLAN '%s' could not be found" % locator)
+
+ vlan_id = found_vlans[0].id
+
+ return driver.ex_get_vlan(vlan_id)
+
+
+def get_mcp_version(driver, location):
+ """
+ Get a locations MCP version
+ """
+ # Get location to determine if MCP 1.0 or 2.0
+ location = driver.ex_get_location_by_id(location)
+ if 'MCP 2.0' in location.name:
+ return '2.0'
+ return '1.0'
+
+
+def is_uuid(u, version=4):
+ """
+ Test if valid v4 UUID
+ """
+ try:
+ uuid_obj = UUID(u, version=version)
+ return str(uuid_obj) == u
+ except:
+ return False
+
+
+def expand_ip_block(block):
+ """
+ Expand public IP block to show all addresses
+ """
+ addresses = []
+ ip_r = block.base_ip.split('.')
+ last_quad = int(ip_r[3])
+ address_root = "%s.%s.%s." % (ip_r[0], ip_r[1], ip_r[2])
+ for i in range(int(block.size)):
+ addresses.append(address_root + str(last_quad + i))
+ return addresses
+
+
+def get_public_ip_block(module, driver, network_domain, block_id=False,
+ base_ip=False):
+ """
+ Get public IP block details
+ """
+ # Block ID given, try to use it.
+ if block_id is not False:
+ try:
+ block = driver.ex_get_public_ip_block(block_id)
+ except DimensionDataAPIException:
+ e = get_exception()
+ # 'UNEXPECTED_ERROR' should be removed once upstream bug is fixed.
+ # Currently any call to ex_get_public_ip_block where the block does
+ # not exist will return UNEXPECTED_ERROR rather than
+ # 'RESOURCE_NOT_FOUND'.
+ if e.code == "RESOURCE_NOT_FOUND" or e.code == 'UNEXPECTED_ERROR':
+ module.exit_json(changed=False, msg="Public IP Block does "
+ "not exist")
+ else:
+ module.fail_json(msg="Unexpected error while retrieving "
+ "block: %s" % e.code)
+ module.fail_json(msg="Error retreving Public IP Block " +
+ "'%s': %s" % (block.id, e.message))
+ # Block ID not given, try to use base_ip.
+ else:
+ blocks = list_public_ip_blocks(module, driver, network_domain)
+ if blocks is not False:
+ block = next(block for block in blocks if block.base_ip == base_ip)
+ else:
+ module.exit_json(changed=False, msg="IP block starting with "
+ "'%s' does not exist." % base_ip)
+ return block
+
+
+def list_nat_rules(module, driver, network_domain):
+ """
+ Get list of NAT rules for domain
+ """
+ try:
+ return driver.ex_list_nat_rules(network_domain)
+ except DimensionDataAPIException:
+ e = get_exception()
+ module.fail_json(msg="Failed to list NAT rules: %s" % e.message)
+
+
+def list_public_ip_blocks(module, driver, network_domain):
+ """
+ Get list of public IP blocks for a domain
+ """
+ try:
+ blocks = driver.ex_list_public_ip_blocks(network_domain)
+ return blocks
+ except DimensionDataAPIException:
+ e = get_exception()
+
+ module.fail_json(msg="Error retreving Public IP Blocks: %s" % e)
+
+
+def get_block_allocation(module, cp_driver, lb_driver, network_domain, block):
+ """
+ Get public IP block allocation details. Shows all ips in block and if
+ they are allocated. Example:
+
+ {'id': 'eb8b16ca-3c91-45fb-b04b-5d7d387a9f4a',
+ 'addresses': [{'address': '162.2.100.100',
+ 'allocated': True
+ },
+ {'address': '162.2.100.101',
+ 'allocated': False
+ }
+ ]
+ }
+ """
+ nat_rules = list_nat_rules(module, cp_driver, network_domain)
+ balancers = list_balancers(module, lb_driver)
+ pub_ip_block = get_public_ip_block(module, cp_driver, network_domain,
+ block.id, False)
+ pub_ips = expand_ip_block(pub_ip_block)
+ block_detailed = {'id': block.id, 'addresses': []}
+ for ip in pub_ips:
+ allocated = False
+
+ nat_match = [nat_rule for nat_rule in nat_rules
+ if nat_rule.external_ip == ip]
+ lb_match = [balancer for balancer in balancers
+ if balancer.ip == ip]
+
+ if len(nat_match) > 0 or len(lb_match) > 0:
+ allocated = True
+ else:
+ allocated = False
+
+ block_detailed['addresses'].append({'address': ip,
+ 'allocated': allocated})
+ return block_detailed
+
+
+def list_balancers(module, lb_driver):
+ try:
+ return lb_driver.list_balancers()
+ except DimensionDataAPIException:
+ e = get_exception()
+
+ module.fail_json(msg="Failed to list Load Balancers: %s" % e.message)
+
+
+def get_blocks_with_unallocated(module, cp_driver, lb_driver, network_domain):
+ """
+ Gets ip blocks with one or more unallocated IPs.
+ ex:
+ {'unallocated_count': ,
+ 'ip_blocks': [],
+ 'unallocated_addresses': []
+ }
+ """
+ total_unallocated_ips = 0
+ all_blocks = list_public_ip_blocks(module, cp_driver, network_domain)
+ unalloc_blocks = []
+ unalloc_addresses = []
+ for block in all_blocks:
+ d_blocks = get_block_allocation(module, cp_driver, lb_driver,
+ network_domain, block)
+ i = 0
+ for addr in d_blocks['addresses']:
+ if addr['allocated'] is False:
+ if i == 0:
+ unalloc_blocks.append(d_blocks)
+ unalloc_addresses.append(addr['address'])
+ total_unallocated_ips += 1
+ i += 1
+ return {'unallocated_count': total_unallocated_ips,
+ 'ip_blocks': unalloc_blocks,
+ 'unallocated_addresses': unalloc_addresses}
+
+
+def get_unallocated_public_ips(module, cp_driver, lb_driver, network_domain,
+ reuse_free, count=0):
+ """
+ Get and/or provision unallocated public IPs
+ """
+ free_ips = []
+ if reuse_free is True:
+ blocks_with_unallocated = get_blocks_with_unallocated(module,
+ cp_driver,
+ lb_driver,
+ network_domain)
+ free_ips = blocks_with_unallocated['unallocated_addresses']
+ if len(free_ips) < count:
+ num_needed = count - len(free_ips)
+ for i in range(num_needed):
+ block = cp_driver.ex_add_public_ip_block_to_network_domain(
+ network_domain)
+ block_dict = get_block_allocation(module, cp_driver, lb_driver,
+ network_domain, block)
+ for addr in block_dict['addresses']:
+ free_ips.append(addr['address'])
+ if len(free_ips) >= count:
+ break
+ return {'changed': True, 'msg': 'Allocated public IP block(s)',
+ 'addresses': free_ips[:count]}
+ else:
+ return {'changed': False, 'msg': 'Found enough unallocated IPs' +
+ ' without provisioning.', 'addresses': free_ips}
+
+
+def is_ipv4_addr(ip):
+ """
+ Simple way to check if IPv4 address
+ """
+ parts = ip.split('.')
+ try:
+ return len(parts) == 4 and all(0 <= int(part) < 256 for part in parts)
+ except:
+ return False
+
+
+def get_node_by_name_and_ip(module, lb_driver, name, ip):
+ """
+ Nodes do not have unique names, we need to match name and IP to be
+ sure we get the correct one
+ """
+ nodes = lb_driver.ex_get_nodes()
+ found_nodes = []
+ if not is_ipv4_addr(ip):
+ module.fail_json(msg="Node '%s' ip is not a valid IPv4 address" % ip)
+
+ found_nodes = [node for node in nodes
+ if node.name == name and node.ip == ip]
+ if len(found_nodes) == 0:
+ return None
+ elif len(found_nodes) == 1:
+ return found_nodes[0]
+ else:
+ module.fail_json(msg="More than one node of name '%s' found." % name)
diff --git a/lib/ansible/modules/cloud/dimensiondata/__init__.py b/lib/ansible/modules/cloud/dimensiondata/__init__.py
new file mode 100644
index 0000000000..e69de29bb2