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