diff --git a/lib/ansible/modules/network/f5/bigip_gtm_topology_record.py b/lib/ansible/modules/network/f5/bigip_gtm_topology_record.py new file mode 100644 index 00000000000..fbcb3cdafd0 --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_gtm_topology_record.py @@ -0,0 +1,1068 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified'} + +DOCUMENTATION = r''' +--- +module: bigip_gtm_topology_record +short_description: Manages GTM Topology Records +description: + - Manages GTM Topology Records. Once created, only topology record C(weight) can be modified. +version_added: 2.8 +options: + source: + description: + - Specifies the origination of an incoming DNS request. + suboptions: + negate: + description: + - When set to c(yes) the system selects this topology record, when the request source does not match. + type: bool + default: no + subnet: + description: + - An IP address and network mask in the CIDR format. + region: + description: + - Specifies the name of region already defined in the configuration. + continent: + description: + - Specifies one of the seven continents, along with the C(Unknown) setting. + - Specifying C(Unknown) forces the system to use a default resolution + if the system cannot determine the location of the local DNS making the request. + - Full continent names and their abbreviated versions are supported. + country: + description: + - Specifies a country. + - Full continent names and their abbreviated versions are supported. + state: + description: + - Specifies a state in a given country. + - This parameter requires country option to be provided. + isp: + description: + - Specifies an Internet service provider. + choices: + - AOL + - BeijingCNC + - CNC + - ChinaEducationNetwork + - ChinaMobilNetwork + - ChinaRailwayTelcom + - ChinaTelecom + - ChinaUnicom + - Comcast + - Earthlink + - ShanghaiCNC + - ShanghaiTelecom + geo_isp: + description: + - Specifies a geolocation ISP + required: True + destination: + description: + - Specifies where the system directs the incoming DNS request. + suboptions: + negate: + description: + - When set to c(yes) the system selects this topology record, when the request destination does not match. + type: bool + default: no + subnet: + description: + - An IP address and network mask in the CIDR format. + region: + description: + - Specifies the name of region already defined in the configuration. + continent: + description: + - Specifies one of the seven continents, along with the C(Unknown) setting. + - Specifying C(Unknown) forces the system to use a default resolution + if the system cannot determine the location of the local DNS making the request. + - Full continent names and their abbreviated versions are supported. + country: + description: + - Specifies a country. + - Full continent names and their abbreviated versions are supported. + state: + description: + - Specifies a state in a given country. + - This parameter requires country option to be provided. + pool: + description: + - Specifies the name of GTM pool already defined in the configuration. + datacenter: + description: + - Specifies the name of GTM data center already defined in the configuration. + isp: + description: + - Specifies an Internet service provider. + choices: + - AOL + - BeijingCNC + - CNC + - ChinaEducationNetwork + - ChinaMobilNetwork + - ChinaRailwayTelcom + - ChinaTelecom + - ChinaUnicom + - Comcast + - Earthlink + - ShanghaiCNC + - ShanghaiTelecom + geo_isp: + description: + - Specifies a geolocation ISP + required: True + weight: + description: + - Specifies the weight of the topology record. + - The system finds the weight of the first topology record that matches the server object (pool or pool member) + and the local DNS. The system then assigns that weight as the topology score for that server object. + - The system load balances to the server object with the highest topology score. + - If the system finds no topology record that matches both the server object and the local DNS, + then the system assigns that server object a zero score. + - If the option is not specified when the record is created the system will set it at a default value of C(1) + - Valid range is (0 - 4294967295) + type: int + partition: + description: + - Device partition to manage resources on. + - Partition parameter is taken into account when used in conjunction with C(pool), C(data_center), + and C(region) parameters, it is ignored otherwise. + default: Common + state: + description: + - When C(state) is C(present), ensures that the record exists. + - When C(state) is C(absent), ensures that the record is removed. + choices: + - present + - absent + default: present +extends_documentation_fragment: f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Create an IP Subnet and an ISP based topology record + bigip_gtm_topology_record: + source: + - subnet: 192.168.1.0/24 + destination: + - isp: AOL + weight: 10 + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a region and a pool based topology record + bigip_gtm_topology_record: + source: + - region: Foo + destination: + - pool: FooPool + partition: FooBar + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost + +- name: Create a negative region and a negative data center based topology record + bigip_gtm_topology_record: + source: + - region: Baz + - negate: yes + destination: + - datacenter: Baz-DC + - negate: yes + provider: + password: secret + server: lb.mydomain.com + user: admin + delegate_to: localhost +''' + +RETURN = r''' +weight: + description: The weight of the topology record. + returned: changed + type: int + sample: 20 +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.six import iteritems + +try: + from library.module_utils.network.f5.bigip import F5RestClient + from library.module_utils.network.f5.common import F5ModuleError + from library.module_utils.network.f5.common import AnsibleF5Parameters + from library.module_utils.network.f5.common import cleanup_tokens + from library.module_utils.network.f5.common import fq_name + from library.module_utils.network.f5.common import f5_argument_spec + from library.module_utils.network.f5.common import exit_json + from library.module_utils.network.f5.common import fail_json + from library.module_utils.network.f5.common import transform_name + from library.module_utils.network.f5.common import flatten_boolean + from library.module_utils.network.f5.ipaddress import is_valid_ip_network +except ImportError: + from ansible.module_utils.network.f5.bigip import F5RestClient + from ansible.module_utils.network.f5.common import F5ModuleError + from ansible.module_utils.network.f5.common import AnsibleF5Parameters + from ansible.module_utils.network.f5.common import cleanup_tokens + from ansible.module_utils.network.f5.common import fq_name + from ansible.module_utils.network.f5.common import f5_argument_spec + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import fail_json + from ansible.module_utils.network.f5.common import transform_name + from ansible.module_utils.network.f5.common import flatten_boolean + from ansible.module_utils.network.f5.ipaddress import is_valid_ip_network + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'score': 'weight', + } + + api_attributes = [ + 'score', + ] + + returnables = [ + 'weight', + 'name' + ] + + updatables = [ + 'weight', + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + countries = { + 'Afghanistan': 'AF', + 'Aland Islands': 'AX', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'American Samoa': 'AS', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Anguilla': 'AI', + 'Antarctica': 'AQ', + 'Antigua and Barbuda': 'AG', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Aruba': 'AW', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bermuda': 'BM', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bonaire, Sint Eustatius and Saba': 'BQ', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Bouvet Island': 'BV', + 'Brazil': 'BR', + 'British Indian Ocean Territory': 'IO', + 'Brunei Darussalam': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cape Verde': 'CV', + 'Cambodia': 'KH', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Cayman Islands': 'KY', + 'Central African Republic': 'CF', + 'Chad': 'TD', + 'Chile': 'CL', + 'China': 'CN', + 'Christmas Island': 'CX', + 'Cocos (Keeling) Islands': 'CC', + 'Colombia': 'CO', + 'Comoros': 'KM', + 'Congo': 'CG', + 'Congo, The Democratic Republic of the': 'CD', + 'Cook Islands': 'CK', + 'Costa Rica': 'CR', + "Cote D'Ivoire": 'CI', + 'Croatia': 'HR', + 'Cuba': 'CU', + 'CuraƧao': 'CW', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Ethiopia': 'ET', + 'Falkland Islands (Malvinas)': 'FK', + 'Faroe Islands': 'FO', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'French Guiana': 'GF', + 'French Polynesia': 'PF', + 'French Southern Territories': 'TF', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Gibraltar': 'GI', + 'Greece': 'GR', + 'Greenland': 'GL', + 'Grenada': 'GD', + 'Guadeloupe': 'GP', + 'Guam': 'GU', + 'Guatemala': 'GT', + 'Guernsey': 'GG', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Heard Island and McDonald Islands': 'HM', + 'Holy See (Vatican City State)': 'VA', + 'Honduras': 'HN', + 'Hong Kong': 'HK', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran, Islamic Republic of': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Isle of Man': 'IM', + 'Israel': 'IL', + 'Italy': 'IT', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jersey': 'JE', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Kenya': 'KE', + 'Kiribati': 'KI', + "Korea, Democratic People's Republic of": 'KP', + 'Korea, Republic of': 'KR', + 'Kuwait': 'KW', + 'Kyrgyzstan': 'KG', + "Lao People's Democratic Republic": 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libyan Arab Jamahiriya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Macau': 'MO', + 'Macedonia': 'MK', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Martinique': 'MQ', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mayotte': 'YT', + 'Mexico': 'MX', + 'Micronesia, Federated States of': 'FM', + 'Moldova, Republic of': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Montserrat': 'MS', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'New Caledonia': 'NC', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'Niue': 'NU', + 'Norfolk Island': 'NF', + 'Northern Mariana Islands': 'MP', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestinian Territory': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Pitcairn Islands': 'PN', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Puerto Rico': 'PR', + 'Qatar': 'QA', + 'Reunion': 'RE', + 'Romania': 'RO', + 'Russian Federation': 'RU', + 'Rwanda': 'RW', + 'Saint Barthelemy': 'BL', + 'Saint Helena': 'SH', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Martin': 'MF', + 'Saint Pierre and Miquelon': 'PM', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Sint Maarten (Dutch part)': 'SX', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Georgia and the South Sandwich Islands': 'GS', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Svalbard and Jan Mayen': 'SJ', + 'Swaziland': 'SZ', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syrian Arab Republic': 'SY', + 'Taiwan': 'TW', + 'Tajikistan': 'TJ', + 'Tanzania, United Republic of': 'TZ', + 'Thailand': 'TH', + 'Timor-Leste': 'TL', + 'Togo': 'TG', + 'Tokelau': 'TK', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Turks and Caicos Islands': 'TC', + 'Tuvalu': 'TV', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'United States Minor Outlying Islands': 'UM', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Virgin Islands, British': 'VG', + 'Virgin Islands, U.S.': 'VI', + 'Wallis and Futuna': 'WF', + 'Western Sahara': 'EH', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW', + 'Unrecognized': 'N/A', + 'Asia/Pacific Region': 'AP', + 'Europe': 'EU', + 'Netherlands Antilles': 'AN', + 'France, Metropolitan': 'FX', + 'Anonymous Proxy': 'A1', + 'Satellite Provider': 'A2', + 'Other': 'O1', + } + + continents = { + 'Antarctica': 'AN', + 'Asia': 'AS', + 'Africa': 'AF', + 'Europe': 'EU', + 'North America': 'NA', + 'South America': 'SA', + 'Oceania': 'OC', + 'Unknown': '--', + } + + @property + def src_negate(self): + src_negate = self._values['source'].get('negate', None) + result = flatten_boolean(src_negate) + if result == 'yes': + return 'not' + return None + + @property + def src_subnet(self): + src_subnet = self._values['source'].get('subnet', None) + if src_subnet is None: + return None + if is_valid_ip_network(src_subnet): + return src_subnet + raise F5ModuleError( + "Specified 'subnet' is not a valid subnet." + ) + + @property + def src_region(self): + src_region = self._values['source'].get('region', None) + if src_region is None: + return None + return fq_name(self.partition, src_region) + + @property + def src_continent(self): + src_continent = self._values['source'].get('continent', None) + if src_continent is None: + return None + result = self.continents.get(src_continent, src_continent) + return result + + @property + def src_country(self): + src_country = self._values['source'].get('country', None) + if src_country is None: + return None + result = self.countries.get(src_country, src_country) + return result + + @property + def src_state(self): + src_country = self._values['source'].get('country', None) + src_state = self._values['source'].get('state', None) + if src_state is None: + return None + if src_country is None: + raise F5ModuleError( + 'Country needs to be provided when specifying state' + ) + result = '{0}/{1}'.format(src_country, src_state) + return result + + @property + def src_isp(self): + src_isp = self._values['source'].get('isp', None) + if src_isp is None: + return None + return fq_name('Common', src_isp) + + @property + def src_geo_isp(self): + src_geo_isp = self._values['source'].get('geo_isp', None) + return src_geo_isp + + @property + def dst_negate(self): + dst_negate = self._values['destination'].get('negate', None) + result = flatten_boolean(dst_negate) + if result == 'yes': + return 'not' + return None + + @property + def dst_subnet(self): + dst_subnet = self._values['destination'].get('subnet', None) + if dst_subnet is None: + return None + if is_valid_ip_network(dst_subnet): + return dst_subnet + raise F5ModuleError( + "Specified 'subnet' is not a valid subnet." + ) + + @property + def dst_region(self): + dst_region = self._values['destination'].get('region', None) + if dst_region is None: + return None + return fq_name(self.partition, dst_region) + + @property + def dst_continent(self): + dst_continent = self._values['destination'].get('continent', None) + if dst_continent is None: + return None + result = self.continents.get(dst_continent, dst_continent) + return result + + @property + def dst_country(self): + dst_country = self._values['destination'].get('country', None) + if dst_country is None: + return None + result = self.countries.get(dst_country, dst_country) + return result + + @property + def dst_state(self): + dst_country = self.dst_country + dst_state = self._values['destination'].get('state', None) + if dst_state is None: + return None + if dst_country is None: + raise F5ModuleError( + 'Country needs to be provided when specifying state' + ) + result = '{0}/{1}'.format(dst_country, dst_state) + return result + + @property + def dst_isp(self): + dst_isp = self._values['destination'].get('isp', None) + if dst_isp is None: + return None + return fq_name('Common', dst_isp) + + @property + def dst_geo_isp(self): + dst_geo_isp = self._values['destination'].get('geo_isp', None) + return dst_geo_isp + + @property + def dst_pool(self): + dst_pool = self._values['destination'].get('pool', None) + if dst_pool is None: + return None + return fq_name(self.partition, dst_pool) + + @property + def dst_datacenter(self): + dst_datacenter = self._values['destination'].get('datacenter', None) + if dst_datacenter is None: + return None + return fq_name(self.partition, dst_datacenter) + + @property + def source(self): + options = { + 'negate': self.src_negate, + 'subnet': self.src_subnet, + 'region': self.src_region, + 'continent': self.src_continent, + 'country': self.src_country, + 'state': self.src_state, + 'isp': self.src_isp, + 'geoip-isp': self.src_geo_isp, + } + result = 'ldns: {0}'.format(self._format_options(options)) + return result + + @property + def destination(self): + options = { + 'negate': self.dst_negate, + 'subnet': self.dst_subnet, + 'region': self.dst_region, + 'continent': self.dst_continent, + 'country': self.dst_country, + 'state': self.dst_state, + 'datacenter': self.dst_datacenter, + 'pool': self.dst_pool, + 'isp': self.dst_isp, + 'geoip-isp': self.dst_geo_isp, + } + result = 'server: {0}'.format(self._format_options(options)) + return result + + @property + def name(self): + result = '{0} {1}'.format(self.source, self.destination) + return result + + def _format_options(self, options): + negate = None + cleaned = dict((k, v) for k, v in iteritems(options) if v is not None) + if 'country' and 'state' in cleaned.keys(): + del cleaned['country'] + if 'negate' in cleaned.keys(): + negate = cleaned['negate'] + del cleaned['negate'] + name, value = cleaned.popitem() + if negate: + result = '{0} {1} {2}'.format(negate, name, value) + return result + result = '{0} {1}'.format(name, value) + return result + + @property + def weight(self): + weight = self._values['weight'] + if weight is None: + return None + if 0 <= weight <= 4294967295: + return weight + raise F5ModuleError( + "Valid weight must be in range 0 - 4294967295" + ) + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + pass + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + changed = False + result = dict() + state = self.want.state + + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def exists(self): + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + + def create_on_device(self): + params = self.changes.api_params() + params['name'] = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + return True + + def update_on_device(self): + params = self.changes.api_params() + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + def remove_from_device(self): + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) + + def read_current_from_device(self): + name = self.want.name + uri = "https://{0}:{1}/mgmt/tm/gtm/topology/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + name.replace(' ', '%20').replace('/', '~') + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + return ApiParameters(params=response) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.choices = [ + 'AOL', 'BeijingCNC', 'CNC', 'ChinaEducationNetwork', + 'ChinaMobilNetwork', 'ChinaRailwayTelcom', 'ChinaTelecom', + 'ChinaUnicom', 'Comcast', 'Earthlink', 'ShanghaiCNC', + 'ShanghaiTelecom', + ] + argument_spec = dict( + source=dict( + required=True, + type='dict', + options=dict( + subnet=dict(), + region=dict(), + continent=dict(), + country=dict(), + state=dict(), + isp=dict( + choices=self.choices + ), + geo_isp=dict(), + negate=dict( + type='bool', + default='no' + ), + ), + mutually_exclusive=[ + ['subnet', 'region', 'continent', 'country', 'isp', 'geo_isp'] + ] + ), + destination=dict( + required=True, + type='dict', + options=dict( + subnet=dict(), + region=dict(), + continent=dict(), + country=dict(), + state=dict(), + pool=dict(), + datacenter=dict(), + isp=dict( + choices=self.choices + ), + geo_isp=dict(), + negate=dict( + type='bool', + default='no' + ), + ), + mutually_exclusive=[ + ['subnet', 'region', 'continent', 'country', 'pool', 'datacenter', 'isp', 'geo_isp'] + ] + ), + weight=dict(type='int'), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + client = F5RestClient(**module.params) + + try: + mm = ModuleManager(module=module, client=client) + results = mm.exec_module() + cleanup_tokens(client) + exit_json(module, results, client) + except F5ModuleError as ex: + cleanup_tokens(client) + fail_json(module, ex, client) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/f5/test_bigip_gtm_topology_record.py b/test/units/modules/network/f5/test_bigip_gtm_topology_record.py new file mode 100644 index 00000000000..5e5605f70cc --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_gtm_topology_record.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +import pytest +import sys + +from nose.plugins.skip import SkipTest +if sys.version_info < (2, 7): + raise SkipTest("F5 Ansible modules require Python >= 2.7") + +from ansible.module_utils.basic import AnsibleModule + +try: + from library.modules.bigip_gtm_topology_record import ApiParameters + from library.modules.bigip_gtm_topology_record import ModuleParameters + from library.modules.bigip_gtm_topology_record import ModuleManager + from library.modules.bigip_gtm_topology_record import ArgumentSpec + + # In Ansible 2.8, Ansible changed import paths. + from test.units.compat import unittest + from test.units.compat.mock import Mock + from test.units.compat.mock import patch + + from test.units.modules.utils import set_module_args +except ImportError: + from ansible.modules.network.f5.bigip_gtm_topology_record import ApiParameters + from ansible.modules.network.f5.bigip_gtm_topology_record import ModuleParameters + from ansible.modules.network.f5.bigip_gtm_topology_record import ModuleManager + from ansible.modules.network.f5.bigip_gtm_topology_record import ArgumentSpec + + # Ansible 2.8 imports + from units.compat import unittest + from units.compat.mock import Mock + from units.compat.mock import patch + + from units.modules.utils import set_module_args + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestParameters(unittest.TestCase): + def test_module_parameters(self): + args = dict( + source=dict( + subnet='192.168.1.0/24', + negate=True + ), + destination=dict( + region='Foobar', + ), + weight=10 + ) + + p = ModuleParameters(params=args) + assert p.name == 'ldns: not subnet 192.168.1.0/24 server: region /Common/Foobar' + assert p.weight == 10 + + def test_api_parameters(self): + args = dict( + source=dict( + subnet='192.168.1.0/24', + negate=True + ), + destination=dict( + region='Foobar', + ), + score=10 + ) + + p = ApiParameters(params=args) + assert p.weight == 10 + + +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_topology_record(self, *args): + set_module_args(dict( + source=dict( + subnet='192.168.1.0/24', + negate=True + ), + destination=dict( + region='Foobar', + ), + weight=10 + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + + # Override methods in the specific type of manager + mm = ModuleManager(module=module) + mm.exists = Mock(side_effect=[False, True]) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True