1#!/usr/bin/python
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17#
18ANSIBLE_METADATA = {'metadata_version': '1.1',
19                    'status': ['preview'],
20                    'supported_by': 'community'}
21
22DOCUMENTATION = '''
23---
24module: ce_rollback
25version_added: "2.4"
26short_description: Set a checkpoint or rollback to a checkpoint on HUAWEI CloudEngine switches.
27description:
28    - This module offers the ability to set a configuration checkpoint
29      file or rollback to a configuration checkpoint file on HUAWEI CloudEngine switches.
30author:
31    - Li Yanfeng (@QijunPan)
32notes:
33    - Recommended connection is C(network_cli).
34    - This module also works with C(local) connections for legacy playbooks.
35options:
36    commit_id:
37        description:
38            - Specifies the label of the configuration rollback point to which system configurations are
39              expected to roll back.
40              The value is an integer that the system generates automatically.
41    label:
42        description:
43            - Specifies a user label for a configuration rollback point.
44              The value is a string of 1 to 256 case-sensitive ASCII characters, spaces not supported.
45              The value must start with a letter and cannot be presented in a single hyphen (-).
46    filename:
47        description:
48            - Specifies a configuration file for configuration rollback.
49              The value is a string of 5 to 64 case-sensitive characters in the format of *.zip, *.cfg, or *.dat,
50              spaces not supported.
51    last:
52        description:
53            - Specifies the number of configuration rollback points.
54              The value is an integer that ranges from 1 to 80.
55    oldest:
56        description:
57            - Specifies the number of configuration rollback points.
58              The value is an integer that ranges from 1 to 80.
59    action:
60        description:
61            - The operation of configuration rollback.
62        required: true
63        choices: ['rollback','clear','set','display','commit']
64'''
65EXAMPLES = '''
66- name: rollback module test
67  hosts: cloudengine
68  connection: local
69  gather_facts: no
70  vars:
71    cli:
72      host: "{{ inventory_hostname }}"
73      port: "{{ ansible_ssh_port }}"
74      username: "{{ username }}"
75      password: "{{ password }}"
76      transport: cli
77
78  tasks:
79
80- name: Ensure commit_id is exist, and specifies the label of the configuration rollback point to
81        which system configurations are expected to roll back.
82  ce_rollback:
83    commit_id: 1000000748
84    action: rollback
85    provider: "{{ cli }}"
86'''
87
88RETURN = '''
89proposed:
90    description: k/v pairs of parameters passed into module
91    returned: sometimes
92    type: dict
93    sample: {"commit_id": "1000000748", "action": "rollback"}
94existing:
95    description: k/v pairs of existing rollback
96    returned: sometimes
97    type: dict
98    sample: {"commitId": "1000000748", "userLabel": "abc"}
99updates:
100    description: command sent to the device
101    returned: always
102    type: list
103    sample: ["rollback configuration to file a.cfg",
104             "set configuration commit 1000000783 label ddd",
105             "clear configuration commit 1000000783 label",
106             "display configuration commit list"]
107changed:
108    description: check to see if a change was made on the device
109    returned: always
110    type: bool
111    sample: true
112end_state:
113    description: k/v pairs of configuration after module execution
114    returned: always
115    type: dict
116    sample: {"commitId": "1000000748", "userLabel": "abc"}
117'''
118
119import re
120from ansible.module_utils.basic import AnsibleModule
121from ansible.module_utils.network.cloudengine.ce import ce_argument_spec, exec_command, run_commands
122from ansible.module_utils.network.common.utils import ComplexList
123
124
125class RollBack(object):
126    """
127    Manages rolls back the system from the current configuration state to a historical configuration state.
128    """
129
130    def __init__(self, argument_spec):
131        self.spec = argument_spec
132        self.module = AnsibleModule(argument_spec=self.spec, supports_check_mode=True)
133        self.commands = list()
134        # module input info
135        self.commit_id = self.module.params['commit_id']
136        self.label = self.module.params['label']
137        self.filename = self.module.params['filename']
138        self.last = self.module.params['last']
139        self.oldest = self.module.params['oldest']
140        self.action = self.module.params['action']
141
142        # state
143        self.changed = False
144        self.updates_cmd = list()
145        self.results = dict()
146        self.existing = dict()
147        self.proposed = dict()
148        self.end_state = dict()
149
150        # configuration rollback points info
151        self.rollback_info = None
152        self.init_module()
153
154    def init_module(self):
155        """ init module """
156
157        required_if = [('action', 'set', ['commit_id', 'label']), ('action', 'commit', ['label'])]
158        mutually_exclusive = None
159        required_one_of = None
160        if self.action == "rollback":
161            required_one_of = [['commit_id', 'label', 'filename', 'last']]
162        elif self.action == "clear":
163            required_one_of = [['commit_id', 'oldest']]
164        self.module = AnsibleModule(
165            argument_spec=self.spec, supports_check_mode=True, required_if=required_if, mutually_exclusive=mutually_exclusive, required_one_of=required_one_of)
166
167    def check_response(self, xml_str, xml_name):
168        """Check if response message is already succeed."""
169
170        if "<ok/>" not in xml_str:
171            self.module.fail_json(msg='Error: %s failed.' % xml_name)
172
173    def cli_add_command(self, command, undo=False):
174        """add command to self.update_cmd and self.commands"""
175        self.commands.append("return")
176        self.commands.append("mmi-mode enable")
177
178        if self.action == "commit":
179            self.commands.append("sys")
180
181        self.commands.append(command)
182        self.updates_cmd.append(command)
183
184    def cli_load_config(self, commands):
185        """load config by cli"""
186
187        if not self.module.check_mode:
188            run_commands(self.module, commands)
189
190    def get_config(self, flags=None):
191        """Retrieves the current config from the device or cache
192        """
193        flags = [] if flags is None else flags
194
195        cmd = 'display configuration '
196        cmd += ' '.join(flags)
197        cmd = cmd.strip()
198
199        rc, out, err = exec_command(self.module, cmd)
200        if rc != 0:
201            self.module.fail_json(msg=err)
202        cfg = str(out).strip()
203
204        return cfg
205
206    def get_rollback_dict(self):
207        """ get rollback attributes dict."""
208
209        rollback_info = dict()
210        rollback_info["RollBackInfos"] = list()
211
212        flags = list()
213        exp = "commit list"
214        flags.append(exp)
215        cfg_info = self.get_config(flags)
216        if not cfg_info:
217            return rollback_info
218
219        cfg_line = cfg_info.split("\n")
220        for cfg in cfg_line:
221            if re.findall(r'^\d', cfg):
222                pre_rollback_info = cfg.split()
223                rollback_info["RollBackInfos"].append(dict(commitId=pre_rollback_info[1], userLabel=pre_rollback_info[2]))
224
225        return rollback_info
226
227    def get_filename_type(self, filename):
228        """Gets the type of filename, such as cfg, zip, dat..."""
229
230        if filename is None:
231            return None
232        if ' ' in filename:
233            self.module.fail_json(
234                msg='Error: Configuration file name include spaces.')
235
236        iftype = None
237
238        if filename.endswith('.cfg'):
239            iftype = 'cfg'
240        elif filename.endswith('.zip'):
241            iftype = 'zip'
242        elif filename.endswith('.dat'):
243            iftype = 'dat'
244        else:
245            return None
246        return iftype.lower()
247
248    def set_config(self):
249
250        if self.action == "rollback":
251            if self.commit_id:
252                cmd = "rollback configuration to commit-id %s" % self.commit_id
253                self.cli_add_command(cmd)
254            if self.label:
255                cmd = "rollback configuration to label %s" % self.label
256                self.cli_add_command(cmd)
257            if self.filename:
258                cmd = "rollback configuration to file %s" % self.filename
259                self.cli_add_command(cmd)
260            if self.last:
261                cmd = "rollback configuration last %s" % self.last
262                self.cli_add_command(cmd)
263        elif self.action == "set":
264            if self.commit_id and self.label:
265                cmd = "set configuration commit %s label %s" % (self.commit_id, self.label)
266                self.cli_add_command(cmd)
267        elif self.action == "clear":
268            if self.commit_id:
269                cmd = "clear configuration commit %s label" % self.commit_id
270                self.cli_add_command(cmd)
271            if self.oldest:
272                cmd = "clear configuration commit oldest %s" % self.oldest
273                self.cli_add_command(cmd)
274        elif self.action == "commit":
275            if self.label:
276                cmd = "commit label %s" % self.label
277                self.cli_add_command(cmd)
278
279        elif self.action == "display":
280            self.rollback_info = self.get_rollback_dict()
281        if self.commands:
282            self.commands.append('return')
283            self.commands.append('undo mmi-mode enable')
284            self.cli_load_config(self.commands)
285            self.changed = True
286
287    def check_params(self):
288        """Check all input params"""
289
290        # commit_id check
291        rollback_info = self.rollback_info["RollBackInfos"]
292        if self.commit_id:
293            if not self.commit_id.isdigit():
294                self.module.fail_json(
295                    msg='Error: The parameter of commit_id is invalid.')
296
297            info_bool = False
298            for info in rollback_info:
299                if info.get("commitId") == self.commit_id:
300                    info_bool = True
301            if not info_bool:
302                self.module.fail_json(
303                    msg='Error: The parameter of commit_id is not exist.')
304
305            if self.action == "clear":
306                info_bool = False
307                for info in rollback_info:
308                    if info.get("commitId") == self.commit_id:
309                        if info.get("userLabel") == "-":
310                            info_bool = True
311                if info_bool:
312                    self.module.fail_json(
313                        msg='Error: This commit_id does not have a label.')
314
315        # filename check
316        if self.filename:
317            if not self.get_filename_type(self.filename):
318                self.module.fail_json(
319                    msg='Error: Invalid file name or file name extension ( *.cfg, *.zip, *.dat ).')
320        # last check
321        if self.last:
322            if not self.last.isdigit():
323                self.module.fail_json(
324                    msg='Error: Number of configuration checkpoints is not digit.')
325            if int(self.last) <= 0 or int(self.last) > 80:
326                self.module.fail_json(
327                    msg='Error: Number of configuration checkpoints is not in the range from 1 to 80.')
328
329        # oldest check
330        if self.oldest:
331            if not self.oldest.isdigit():
332                self.module.fail_json(
333                    msg='Error: Number of configuration checkpoints is not digit.')
334            if int(self.oldest) <= 0 or int(self.oldest) > 80:
335                self.module.fail_json(
336                    msg='Error: Number of configuration checkpoints is not in the range from 1 to 80.')
337
338        # label check
339        if self.label:
340            if self.label[0].isdigit():
341                self.module.fail_json(
342                    msg='Error: Commit label which should not start with a number.')
343            if len(self.label.replace(' ', '')) == 1:
344                if self.label == '-':
345                    self.module.fail_json(
346                        msg='Error: Commit label which should not be "-"')
347            if len(self.label.replace(' ', '')) < 1 or len(self.label) > 256:
348                self.module.fail_json(
349                    msg='Error: Label of configuration checkpoints is a string of 1 to 256 characters.')
350
351            if self.action == "rollback":
352                info_bool = False
353                for info in rollback_info:
354                    if info.get("userLabel") == self.label:
355                        info_bool = True
356                if not info_bool:
357                    self.module.fail_json(
358                        msg='Error: The parameter of userLabel is not exist.')
359
360            if self.action == "commit":
361                info_bool = False
362                for info in rollback_info:
363                    if info.get("userLabel") == self.label:
364                        info_bool = True
365                if info_bool:
366                    self.module.fail_json(
367                        msg='Error: The parameter of userLabel is existing.')
368
369            if self.action == "set":
370                info_bool = False
371                for info in rollback_info:
372                    if info.get("commitId") == self.commit_id:
373                        if info.get("userLabel") != "-":
374                            info_bool = True
375                if info_bool:
376                    self.module.fail_json(
377                        msg='Error: The userLabel of this commitid is present and can be reset after deletion.')
378
379    def get_proposed(self):
380        """get proposed info"""
381
382        if self.commit_id:
383            self.proposed["commit_id"] = self.commit_id
384        if self.label:
385            self.proposed["label"] = self.label
386        if self.filename:
387            self.proposed["filename"] = self.filename
388        if self.last:
389            self.proposed["last"] = self.last
390        if self.oldest:
391            self.proposed["oldest"] = self.oldest
392
393    def get_existing(self):
394        """get existing info"""
395        if not self.rollback_info:
396            self.existing["RollBackInfos"] = None
397        else:
398            self.existing["RollBackInfos"] = self.rollback_info["RollBackInfos"]
399
400    def get_end_state(self):
401        """get end state info"""
402
403        rollback_info = self.get_rollback_dict()
404        if not rollback_info:
405            self.end_state["RollBackInfos"] = None
406        else:
407            self.end_state["RollBackInfos"] = rollback_info["RollBackInfos"]
408
409    def work(self):
410        """worker"""
411
412        self.rollback_info = self.get_rollback_dict()
413        self.check_params()
414        self.get_proposed()
415
416        self.set_config()
417
418        self.get_existing()
419        self.get_end_state()
420
421        self.results['changed'] = self.changed
422        self.results['proposed'] = self.proposed
423        self.results['existing'] = self.existing
424        self.results['end_state'] = self.end_state
425        if self.changed:
426            self.results['updates'] = self.updates_cmd
427        else:
428            self.results['updates'] = list()
429
430        self.module.exit_json(**self.results)
431
432
433def main():
434    """Module main"""
435
436    argument_spec = dict(
437        commit_id=dict(required=False),
438        label=dict(required=False, type='str'),
439        filename=dict(required=False, type='str'),
440        last=dict(required=False, type='str'),
441        oldest=dict(required=False, type='str'),
442        action=dict(required=False, type='str', choices=[
443            'rollback', 'clear', 'set', 'commit', 'display']),
444    )
445    argument_spec.update(ce_argument_spec)
446    module = RollBack(argument_spec)
447    module.work()
448
449
450if __name__ == '__main__':
451    main()
452