diff --git a/lib/ansible/module_utils/aci.py b/lib/ansible/module_utils/aci.py index cbf2e05f24..7210f47fd7 100644 --- a/lib/ansible/module_utils/aci.py +++ b/lib/ansible/module_utils/aci.py @@ -78,6 +78,7 @@ URL_MAPPING = dict( fc_policy=dict(aci_class='fcIfPol', mo='infra/fcIfPol-', key='name'), filter=dict(aci_class='vzFilter', mo='flt-', key='name'), gateway_addr=dict(aci_class='fvSubnet', mo='subnet-', key='ip'), + import_policy=dict(aci_class='configImportP', mo='fabric/configimp-', key='name'), l2_policy=dict(aci_class='l2IfPol', mo='infra/l2IfP-', key='name'), lldp_policy=dict(aci_class='lldpIfPol', mo='infra/lldpIfP-', key='name'), mcp=dict(aci_class='mcpIfPol', mo='infra/mcpIfP-', key='name'), diff --git a/lib/ansible/modules/network/aci/aci_config_rollback.py b/lib/ansible/modules/network/aci/aci_config_rollback.py new file mode 100644 index 0000000000..093b7867a5 --- /dev/null +++ b/lib/ansible/modules/network/aci/aci_config_rollback.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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': 'community'} + +DOCUMENTATION = r''' +--- +module: aci_config_rollback +short_description: Provides rollback and rollback preview functionality for Cisco ACI fabrics (config:ImportP) +description: +- Provides rollback and rollback preview functionality for Cisco ACI fabric. +- Config Rollbacks are done using snapshots C(aci_snapshot) with the configImportP class. +- More information from the internal APIC class + I(config:ImportP) at U(https://developer.cisco.com/media/mim-ref/MO-configImportP.html). +author: +- Swetha Chunduri (@schunduri) +- Dag Wieers (@dagwieers) +- Jacob McGill (@jmcgill298) +version_added: '2.4' +requirements: +- ACI Fabric 1.0(3f)+ +options: + compare_export_policy: + description: + - The export policy that the C(compare_snapshot) is associated to. + compare_snapshot: + description: + - The name of the snapshot to compare with C(snapshot). + description: + description: + - The description for the Import Policy. + aliases: [ descr ] + export_policy: + description: + - The export policy that the C(snapshot) is associated to. + required: yes + fail_on_decrypt: + description: + - Determines if the APIC should fail the rollback if unable to decrypt secured data. + - The APIC defaults new Import Policies to C(yes). + type: bool + default: 'yes' + import_mode: + description: + - Determines how the import should be handled by the APIC. + - The APIC defaults new Import Policies to C(atomic). + choices: [ atomic, best-effort ] + default: atomic + import_policy: + description: + - The name of the Import Policy to use for config rollback. + import_type: + description: + - Determines how the current and snapshot configuration should be compared for replacement. + - The APIC defaults new Import Policies to C(replace). + choices: [ merge, replace ] + default: replace + snapshot: + description: + - The name of the snapshot to rollback to, or the base snapshot to use for comparison. + - The C(aci_snapshot) module can be used to query the list of available snapshots. + required: yes + state: + description: + - Use C(preview) for previewing the diff between two snapshots. + - Use C(rollback) for reverting the configuration to a previous snapshot. + choices: [ preview, rollback ] + default: rollback +extends_documentation_fragment: aci +''' + +EXAMPLES = r''' +--- +- name: Create a Snapshot + aci_config_snapshot: + hostname: apic + username: admin + password: SomeSecretPassword + state: present + export_policy: config_backup + +- name: Query Existing Snapshots + aci_config_snapshot: + hostname: apic + username: admin + password: SomeSecretPassword + state: query + export_policy: config_backup + +- name: Compare Snapshot Files + aci_config_rollback: + hostname: apic + username: admin + password: SomeSecretPassword + state: preview + export_policy: config_backup + snapshot: 'run-2017-08-28T06-24-01' + compare_export_policy: config_backup + compare_snapshot: 'run-2017-08-27T23-43-56' + +- name: Rollback Configuration + aci_config_rollback: + hostname: apic + username: admin + password: SomeSecretPassword + state: rollback + import_policy: rollback_config + export_policy: config_backup + snapshot: 'run-2017-08-28T06-24-01' + +- name: Rollback Configuration + aci_config_rollback: + hostname: apic + username: admin + password: SomeSecretPassword + state: rollback + import_policy: rollback_config + export_policy: config_backup + snapshot: 'run-2017-08-28T06-24-01' + description: 'Rollback 8-27 changes' + import_mode: atomic + import_type: replace + fail_on_decrypt: yes +''' + +RETURN = r''' +# +''' + +from ansible.module_utils.aci import ACIModule, aci_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_bytes +from ansible.module_utils.urls import fetch_url + +# Optional, only used for rollback preview +try: + import lxml.etree + from xmljson import cobra + XML_TO_JSON = True +except ImportError: + XML_TO_JSON = False + + +def main(): + argument_spec = aci_argument_spec + argument_spec.update( + compare_export_policy=dict(type='str'), + compare_snapshot=dict(type='str'), + description=dict(type='str', aliases=['descr']), + export_policy=dict(type='str'), + fail_on_decrypt=dict(type='bool'), + import_mode=dict(type='str', choices=['atomic', 'best-effort']), + import_policy=dict(type='str'), + import_type=dict(type='str', choices=['merge', 'replace']), + snapshot=dict(type='str', required=True), + state=dict(type='str', default='rollback', choices=['preview', 'rollback']), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False, + required_if=[ + ['state', 'preview', ['compare_export_policy', 'compare_snapshot']], + ['state', 'rollback', ['import_policy']], + ], + ) + + description = module.params['description'] + export_policy = module.params['export_policy'] + fail_on_decrypt = module.params['fail_on_decrypt'] + if fail_on_decrypt is True: + fail_on_decrypt = 'yes' + elif fail_on_decrypt is False: + fail_on_decrypt = 'no' + import_mode = module.params['import_mode'] + import_policy = module.params['import_policy'] + import_type = module.params['import_type'] + snapshot = module.params['snapshot'] + state = module.params['state'] + + aci = ACIModule(module) + + if state == 'rollback': + if snapshot.startswith('run-'): + snapshot = snapshot.replace('run-', '', 1) + + if not snapshot.endswith('.tar.gz'): + snapshot += '.tar.gz' + + filename = 'ce2_{0}-{1}'.format(export_policy, snapshot) + + aci.construct_url(root_class="import_policy") + aci.get_existing() + + # Filter out module parameters with null values + aci.payload( + aci_class='configImportP', + class_config=dict( + adminSt='triggered', + descr=description, + failOnDecryptErrors=fail_on_decrypt, + fileName=filename, + importMode=import_mode, + importType=import_type, + name=import_policy, + snapshot='yes', + ), + ) + + # Generate config diff which will be used as POST request body + aci.get_diff(aci_class='configImportP') + + # Submit changes if module not in check_mode and the proposed is different than existing + aci.post_config() + + elif state == 'preview': + aci.result['url'] = '%(protocol)s://%(hostname)s/mqapi2/snapshots.diff.xml' % module.params + aci.result['filter_string'] = ( + '?s1dn=uni/backupst/snapshots-[uni/fabric/configexp-%(export_policy)s]/snapshot-%(snapshot)s&' + 's2dn=uni/backupst/snapshots-[uni/fabric/configexp-%(compare_export_policy)s]/snapshot-%(compare_snapshot)s' + ) % module.params + + # Generate rollback comparison + get_preview(aci) + + module.exit_json(**aci.result) + + +def get_preview(aci): + ''' + This function is used to generate a preview between two snapshots and add the parsed results to the aci module return data. + ''' + uri = aci.result['url'] + aci.result['filter_string'] + resp, info = fetch_url(aci.module, uri, headers=aci.headers, method='GET', timeout=aci.module.params['timeout'], use_proxy=aci.module.params['use_proxy']) + aci.result['response'] = info['msg'] + aci.result['status'] = info['status'] + aci.result['method'] = 'GET' + + # Handle APIC response + if info['status'] == 200: + xml_to_json(aci, resp.read()) + else: + aci.result['apic_response'] = resp.read() + aci.module.fail_json(msg='Request failed: %(error_code)s %(error_text)s' % aci.result, **aci.result) + + +def xml_to_json(aci, response_data): + ''' + This function is used to convert preview XML data into JSON. + ''' + if XML_TO_JSON: + xml = lxml.etree.fromstring(to_bytes(response_data)) + xmldata = cobra.data(xml) + aci.result['diff'] = xmldata + else: + aci.result['diff'] = response_data + + +if __name__ == "__main__": + main()