diff --git a/lib/ansible/modules/network/cloudengine/ce_rollback.py b/lib/ansible/modules/network/cloudengine/ce_rollback.py new file mode 100644 index 00000000000..95ad587f4b3 --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_rollback.py @@ -0,0 +1,522 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . +# +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0'} + +DOCUMENTATION = ''' +--- +module: ce_rollback +version_added: "2.4" +short_description: Set a checkpoint or rollback to a checkpoint on HUAWEI CloudEngine switches. +description: + - This module offers the ability to set a configuration checkpoint + file or rollback to a configuration checkpoint file on HUAWEI CloudEngine switches. +author: + - Li Yanfeng (@CloudEngine-Ansible) +options: + commit_id: + description: + - Specifies the label of the configuration rollback point to which system configurations are + expected to roll back. + The value is an integer that the system generates automatically. + required: false + label: + description: + - Specifies a user label for a configuration rollback point. + The value is a string of 1 to 256 case-sensitive ASCII characters, spaces not supported. + The value must start with a letter and cannot be presented in a single hyphen (-). + required: false + default: null + filename: + description: + - Specifies a configuration file for configuration rollback. + The value is a string of 5 to 64 case-sensitive characters in the format of *.zip, *.cfg, or *.dat, + spaces not supported. + required: false + default: null + last: + description: + - Specifies the number of configuration rollback points. + The value is an integer that ranges from 1 to 80. + required: false + default: null + oldest: + description: + - Specifies the number of configuration rollback points. + The value is an integer that ranges from 1 to 80. + required: false + default: null + action: + description: + - The operation of configuration rollback. + required: true + choices: ['rollback','clear','set','display','commit'] +''' +EXAMPLES = ''' +- name: rollback module test + hosts: cloudengine + connection: local + gather_facts: no + vars: + cli: + host: "{{ inventory_hostname }}" + port: "{{ ansible_ssh_port }}" + username: "{{ username }}" + password: "{{ password }}" + transport: cli + + tasks: + +- name: Ensure commit_id is exist, and specifies the label of the configuration rollback point to + which system configurations are expected to roll back. + ce_rollback: + commit_id: 1000000748 + action: rollback + provider: "{{ cli }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: sometimes + type: dict + sample: {"commit_id": "1000000748", "action": "rollback"} +existing: + description: k/v pairs of existing rollback + returned: sometimes + type: dict + sample: {"commitId": "1000000748", "userLabel": "abc"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["rollback configuration to file a.cfg", + "set configuration commit 1000000783 label ddd", + "clear configuration commit 1000000783 label", + "display configuration commit list"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"commitId": "1000000748", "userLabel": "abc"} +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ce import get_nc_config, execute_nc_action, ce_argument_spec, run_commands + + +CE_NC_GET_CHECKPOINT = """ + + + + + + + + + + + +""" + +CE_NC_ACTION_ROLLBACK_COMMIT_ID = """ + + + + %s + + + +""" + +CE_NC_ACTION_ROLLBACK_LABEL = """ + + + + %s + + + +""" + +CE_NC_ACTION_ROLLBACK_LAST = """ + + + + %s + + + +""" + +CE_NC_ACTION_ROLLBACK_FILE = """ + + + + %s + + + +""" + +CE_NC_ACTION_SET_COMMIT_ID_LABEL = """ + + + + %s + %s + + + +""" + +CE_NC_ACTION_CLEAR_COMMIT_ID_LABEL = """ + + + + %s + + + +""" + +CE_NC_ACTION_CLEAR_OLDEST_COMMIT_ID = """ + + + + %s + + + +""" + + +class RollBack(object): + """ + Manages rolls back the system from the current configuration state to a historical configuration state. + """ + + def __init__(self, argument_spec): + self.spec = argument_spec + self.module = None + self.init_module() + + # module input info + self.commit_id = self.module.params['commit_id'] + self.label = self.module.params['label'] + self.filename = self.module.params['filename'] + self.last = self.module.params['last'] + self.oldest = self.module.params['oldest'] + self.action = self.module.params['action'] + + # state + self.changed = False + self.updates_cmd = list() + self.results = dict() + self.existing = dict() + self.proposed = dict() + self.end_state = dict() + + # configuration rollback points info + self.rollback_info = None + + def init_module(self): + """ init module """ + + self.module = AnsibleModule( + argument_spec=self.spec, supports_check_mode=True) + + def check_response(self, xml_str, xml_name): + """Check if response message is already succeed.""" + + if "" not in xml_str: + self.module.fail_json(msg='Error: %s failed.' % xml_name) + + def get_rollback_dict(self): + """ get rollback attributes dict.""" + + rollback_info = dict() + conf_str = CE_NC_GET_CHECKPOINT + xml_str = get_nc_config(self.module, conf_str) + rollback_info["RollBackInfos"] = list() + if "" in xml_str: + return rollback_info + else: + re_find = re.findall(r'.*(.*).*\s*' + r'(.*).*\s*' + r'(.*).*', xml_str) + + for mem in re_find: + rollback_info["RollBackInfos"].append( + dict(commitId=mem[0], userLabel=mem[2])) + return rollback_info + + def get_filename_type(self, filename): + """Gets the type of filename, such as cfg, zip, dat...""" + + if filename is None: + return None + if ' ' in filename: + self.module.fail_json( + msg='Error: Configuration file name include spaces.') + + iftype = None + + if filename.endswith('.cfg'): + iftype = 'cfg' + elif filename.endswith('.zip'): + iftype = 'zip' + elif filename.endswith('.dat'): + iftype = 'dat' + else: + return None + return iftype.lower() + + def rollback_commit_id(self): + """rollback comit_id""" + + cfg_xml = "" + self.updates_cmd.append( + "rollback configuration to commit-id %s" % self.commit_id) + cfg_xml = CE_NC_ACTION_ROLLBACK_COMMIT_ID % self.commit_id + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "ROLLBACK_COMMITID") + self.changed = True + + def rollback_label(self): + """rollback label""" + + cfg_xml = "" + self.updates_cmd.append( + "rollback configuration to label %s" % self.label) + cfg_xml = CE_NC_ACTION_ROLLBACK_LABEL % self.label + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "ROLLBACK_LABEL") + self.changed = True + + def rollback_filename(self): + """rollback filename""" + + cfg_xml = "" + self.updates_cmd.append( + "rollback configuration to file %s" % self.filename) + cfg_xml = CE_NC_ACTION_ROLLBACK_FILE % self.filename + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "ROLLBACK_FILENAME") + self.changed = True + + def rollback_last(self): + """rollback last""" + + cfg_xml = "" + self.updates_cmd.append( + "rollback configuration to last %s" % self.last) + cfg_xml = CE_NC_ACTION_ROLLBACK_LAST % self.last + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "ROLLBACK_LAST") + self.changed = True + + def set_commitid_label(self): + """set commitid label""" + + cfg_xml = "" + self.updates_cmd.append( + "set configuration commit %s label %s" % (self.commit_id, self.label)) + cfg_xml = CE_NC_ACTION_SET_COMMIT_ID_LABEL % ( + self.commit_id, self.label) + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "SET_COMIMIT_LABEL") + self.changed = True + + def clear_commitid_label(self): + """clear commitid label""" + + cfg_xml = "" + self.updates_cmd.append( + "clear configuration commit %s label" % self.commit_id) + cfg_xml = CE_NC_ACTION_CLEAR_COMMIT_ID_LABEL % self.commit_id + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "CLEAR_COMMIT_LABEL") + self.changed = True + + def clear_oldest(self): + """clear oldest""" + + cfg_xml = "" + self.updates_cmd.append( + "clear configuration commit oldest %s" % self.oldest) + cfg_xml = CE_NC_ACTION_CLEAR_OLDEST_COMMIT_ID % self.oldest + recv_xml = execute_nc_action(self.module, cfg_xml) + self.check_response(recv_xml, "CLEAR_COMMIT_OLDEST") + self.changed = True + + def commit_label(self): + """commit label""" + + commands = list() + cmd1 = {'output': None, 'command': 'system-view'} + commands.append(cmd1) + + cmd2 = {'output': None, 'command': ''} + cmd2['command'] = "commit label %s" % self.label + commands.append(cmd2) + self.updates_cmd.append( + "commit label %s" % self.label) + run_commands(self.module, commands) + self.changed = True + + def check_params(self): + """Check all input params""" + + # commit_id check + if self.commit_id: + if not self.commit_id.isdigit(): + self.module.fail_json( + msg='Error: The parameter of commit_id is invalid.') + + # label check + if self.label: + if self.label[0].isdigit(): + self.module.fail_json( + msg='Error: Commit label which should not start with a number.') + if len(self.label.replace(' ', '')) == 1: + if cmp(self.label, '-') == 0: + self.module.fail_json( + msg='Error: Commit label which should not be "-"') + if len(self.label.replace(' ', '')) < 1 or len(self.label) > 256: + self.module.fail_json( + msg='Error: Label of configuration checkpoints is a string of 1 to 256 characters.') + + # filename check + if self.filename: + if not self.get_filename_type(self.filename): + self.module.fail_json( + msg='Error: Invalid file name or file name extension ( *.cfg, *.zip, *.dat ).') + # last check + if self.last: + if not self.last.isdigit(): + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not digit.') + if int(self.last) <= 0 or int(self.last) > 80: + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not in the range from 1 to 80.') + + # oldest check + if self.oldest: + if not self.oldest.isdigit(): + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not digit.') + if int(self.oldest) <= 0 or int(self.oldest) > 80: + self.module.fail_json( + msg='Error: Number of configuration checkpoints is not in the range from 1 to 80.') + + def get_proposed(self): + """get proposed info""" + + if self.commit_id: + self.proposed["commit_id"] = self.commit_id + if self.label: + self.proposed["label"] = self.label + if self.filename: + self.proposed["filename"] = self.filename + if self.last: + self.proposed["last"] = self.last + if self.oldest: + self.proposed["oldest"] = self.oldest + + def get_existing(self): + """get existing info""" + + if not self.rollback_info: + return + self.existing["RollBackInfos"] = self.rollback_info["RollBackInfos"] + + def get_end_state(self): + """get end state info""" + + self.end_state = None + + def work(self): + """worker""" + + self.check_params() + self.get_proposed() + # action mode + if self.action == "rollback": + if self.commit_id: + self.rollback_commit_id() + if self.label: + self.rollback_label() + if self.filename: + self.rollback_filename() + if self.last: + self.rollback_last() + elif self.action == "set": + if self.commit_id and self.label: + self.set_commitid_label() + elif self.action == "clear": + if self.commit_id: + self.clear_commitid_label() + if self.oldest: + self.clear_oldest() + elif self.action == "commit": + if self.label: + self.commit_label() + elif self.action == "display": + self.rollback_info = self.get_rollback_dict() + + self.get_existing() + self.get_end_state() + self.results['changed'] = self.changed + self.results['proposed'] = self.proposed + self.results['existing'] = self.existing + self.results['end_state'] = self.end_state + if self.changed: + self.results['updates'] = self.updates_cmd + else: + self.results['updates'] = list() + + self.module.exit_json(**self.results) + + +def main(): + """Module main""" + + argument_spec = dict( + commit_id=dict(required=False), + label=dict(required=False, type='str'), + filename=dict(required=False, type='str'), + last=dict(required=False, type='str'), + oldest=dict(required=False, type='str'), + action=dict(required=False, type='str', choices=[ + 'rollback', 'clear', 'set', 'commit', 'display']), + ) + argument_spec.update(ce_argument_spec) + module = RollBack(argument_spec) + module.work() + + +if __name__ == '__main__': + main()