1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# Copyright (C) 2017 Lenovo, Inc. 5# (c) 2017, Ansible by Red Hat, inc 6# This file is part of Ansible 7# 8# Ansible is free software: you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation, either version 3 of the License, or 11# (at your option) any later version. 12# 13# Ansible is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 20# 21# Module to work on Interfaces with Lenovo Switches 22# Lenovo Networking 23# 24 25from __future__ import absolute_import, division, print_function 26__metaclass__ = type 27 28 29ANSIBLE_METADATA = {'metadata_version': '1.1', 30 'status': ['preview'], 31 'supported_by': 'community'} 32 33 34DOCUMENTATION = """ 35--- 36module: cnos_interface 37version_added: "2.3" 38author: "Anil Kumar Muraleedharan(@amuraleedhar)" 39short_description: Manage Interface on Lenovo CNOS network devices 40description: 41 - This module provides declarative management of Interfaces 42 on Lenovo CNOS network devices. 43notes: 44 - Tested against CNOS 10.8.1 45options: 46 name: 47 description: 48 - Name of the Interface. 49 required: true 50 version_added: "2.8" 51 description: 52 description: 53 - Description of Interface. 54 version_added: "2.8" 55 enabled: 56 description: 57 - Interface link status. 58 type: bool 59 default: True 60 version_added: "2.8" 61 speed: 62 description: 63 - Interface link speed. 64 version_added: "2.8" 65 mtu: 66 description: 67 - Maximum size of transmit packet. 68 version_added: "2.8" 69 duplex: 70 description: 71 - Interface link status 72 default: auto 73 choices: ['full', 'half', 'auto'] 74 version_added: "2.8" 75 tx_rate: 76 description: 77 - Transmit rate in bits per second (bps). 78 - This is state check parameter only. 79 - Supports conditionals, see L(Conditionals in Networking Modules, 80 ../network/user_guide/network_working_with_command_output.html) 81 version_added: "2.8" 82 rx_rate: 83 description: 84 - Receiver rate in bits per second (bps). 85 - This is state check parameter only. 86 - Supports conditionals, see L(Conditionals in Networking Modules, 87 ../network/user_guide/network_working_with_command_output.html) 88 version_added: "2.8" 89 neighbors: 90 description: 91 - Check operational state of given interface C(name) for LLDP neighbor. 92 - The following suboptions are available. 93 version_added: "2.8" 94 suboptions: 95 host: 96 description: 97 - "LLDP neighbor host for given interface C(name)." 98 port: 99 description: 100 - "LLDP neighbor port to which interface C(name) is connected." 101 aggregate: 102 description: List of Interfaces definitions. 103 version_added: "2.8" 104 delay: 105 description: 106 - Time in seconds to wait before checking for the operational state on 107 remote device. This wait is applicable for operational state argument 108 which are I(state) with values C(up)/C(down), I(tx_rate) and I(rx_rate) 109 default: 20 110 version_added: "2.8" 111 state: 112 description: 113 - State of the Interface configuration, C(up) means present and 114 operationally up and C(down) means present and operationally C(down) 115 default: present 116 version_added: "2.8" 117 choices: ['present', 'absent', 'up', 'down'] 118 provider: 119 description: 120 - B(Deprecated) 121 - "Starting with Ansible 2.5 we recommend using C(connection: network_cli)." 122 - For more information please see the L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html). 123 - HORIZONTALLINE 124 - A dict object containing connection details. 125 version_added: "2.8" 126 suboptions: 127 host: 128 description: 129 - Specifies the DNS host name or address for connecting to the remote 130 device over the specified transport. The value of host is used as 131 the destination address for the transport. 132 required: true 133 port: 134 description: 135 - Specifies the port to use when building the connection to the remote device. 136 default: 22 137 username: 138 description: 139 - Configures the username to use to authenticate the connection to 140 the remote device. This value is used to authenticate 141 the SSH session. If the value is not specified in the task, the 142 value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead. 143 password: 144 description: 145 - Specifies the password to use to authenticate the connection to 146 the remote device. This value is used to authenticate 147 the SSH session. If the value is not specified in the task, the 148 value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead. 149 timeout: 150 description: 151 - Specifies the timeout in seconds for communicating with the network device 152 for either connecting or sending commands. If the timeout is 153 exceeded before the operation is completed, the module will error. 154 default: 10 155 ssh_keyfile: 156 description: 157 - Specifies the SSH key to use to authenticate the connection to 158 the remote device. This value is the path to the 159 key used to authenticate the SSH session. If the value is not specified 160 in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) 161 will be used instead. 162 authorize: 163 description: 164 - Instructs the module to enter privileged mode on the remote device 165 before sending any commands. If not specified, the device will 166 attempt to execute all commands in non-privileged mode. If the value 167 is not specified in the task, the value of environment variable 168 C(ANSIBLE_NET_AUTHORIZE) will be used instead. 169 type: bool 170 default: 'no' 171 auth_pass: 172 description: 173 - Specifies the password to use if required to enter privileged mode 174 on the remote device. If I(authorize) is false, then this argument 175 does nothing. If the value is not specified in the task, the value of 176 environment variable C(ANSIBLE_NET_AUTH_PASS) will be used instead. 177""" 178 179EXAMPLES = """ 180- name: configure interface 181 cnos_interface: 182 name: Ethernet1/33 183 description: test-interface 184 speed: 100 185 duplex: half 186 mtu: 999 187 188- name: remove interface 189 cnos_interface: 190 name: loopback3 191 state: absent 192 193- name: make interface up 194 cnos_interface: 195 name: Ethernet1/33 196 enabled: True 197 198- name: make interface down 199 cnos_interface: 200 name: Ethernet1/33 201 enabled: False 202 203- name: Check intent arguments 204 cnos_interface: 205 name: Ethernet1/33 206 state: up 207 tx_rate: ge(0) 208 rx_rate: le(0) 209 210- name: Check neighbors intent arguments 211 cnos_interface: 212 name: Ethernet1/33 213 neighbors: 214 - port: eth0 215 host: netdev 216 217- name: Config + intent 218 cnos_interface: 219 name: Ethernet1/33 220 enabled: False 221 state: down 222 223- name: Add interface using aggregate 224 cnos_interface: 225 aggregate: 226 - { name: Ethernet1/33, mtu: 256, description: test-interface-1 } 227 - { name: Ethernet1/44, mtu: 516, description: test-interface-2 } 228 duplex: full 229 speed: 100 230 state: present 231 232- name: Delete interface using aggregate 233 cnos_interface: 234 aggregate: 235 - name: loopback3 236 - name: loopback6 237 state: absent 238""" 239 240RETURN = """ 241commands: 242 description: The list of configuration mode commands to send to the device. 243 returned: always, except for the platforms that use Netconf transport to 244 manage the device. 245 type: list 246 sample: 247 - interface Ethernet1/33 248 - description test-interface 249 - duplex half 250 - mtu 512 251""" 252import re 253 254from copy import deepcopy 255from time import sleep 256 257from ansible.module_utils._text import to_text 258from ansible.module_utils.basic import AnsibleModule 259from ansible.module_utils.connection import exec_command 260from ansible.module_utils.network.cnos.cnos import get_config, load_config 261from ansible.module_utils.network.cnos.cnos import cnos_argument_spec 262from ansible.module_utils.network.cnos.cnos import debugOutput, check_args 263from ansible.module_utils.network.common.config import NetworkConfig 264from ansible.module_utils.network.common.utils import conditional 265from ansible.module_utils.network.common.utils import remove_default_spec 266 267 268def validate_mtu(value, module): 269 if value and not 64 <= int(value) <= 9216: 270 module.fail_json(msg='mtu must be between 64 and 9216') 271 272 273def validate_param_values(module, obj, param=None): 274 if param is None: 275 param = module.params 276 for key in obj: 277 # validate the param value (if validator func exists) 278 validator = globals().get('validate_%s' % key) 279 if callable(validator): 280 validator(param.get(key), module) 281 282 283def parse_shutdown(configobj, name): 284 cfg = configobj['interface %s' % name] 285 cfg = '\n'.join(cfg.children) 286 match = re.search(r'^shutdown', cfg, re.M) 287 if match: 288 return True 289 else: 290 return False 291 292 293def parse_config_argument(configobj, name, arg=None): 294 cfg = configobj['interface %s' % name] 295 cfg = '\n'.join(cfg.children) 296 match = re.search(r'%s (.+)$' % arg, cfg, re.M) 297 if match: 298 return match.group(1) 299 300 301def search_obj_in_list(name, lst): 302 for o in lst: 303 if o['name'] == name: 304 return o 305 306 return None 307 308 309def add_command_to_interface(interface, cmd, commands): 310 if interface not in commands: 311 commands.append(interface) 312 commands.append(cmd) 313 314 315def map_config_to_obj(module): 316 config = get_config(module) 317 configobj = NetworkConfig(indent=1, contents=config) 318 319 match = re.findall(r'^interface (\S+)', config, re.M) 320 if not match: 321 return list() 322 323 instances = list() 324 325 for item in set(match): 326 obj = { 327 'name': item, 328 'description': parse_config_argument(configobj, item, 'description'), 329 'speed': parse_config_argument(configobj, item, 'speed'), 330 'duplex': parse_config_argument(configobj, item, 'duplex'), 331 'mtu': parse_config_argument(configobj, item, 'mtu'), 332 'disable': True if parse_shutdown(configobj, item) else False, 333 'state': 'present' 334 } 335 instances.append(obj) 336 return instances 337 338 339def map_params_to_obj(module): 340 obj = [] 341 aggregate = module.params.get('aggregate') 342 if aggregate: 343 for item in aggregate: 344 for key in item: 345 if item.get(key) is None: 346 item[key] = module.params[key] 347 348 validate_param_values(module, item, item) 349 d = item.copy() 350 351 if d['enabled']: 352 d['disable'] = False 353 else: 354 d['disable'] = True 355 356 obj.append(d) 357 358 else: 359 params = { 360 'name': module.params['name'], 361 'description': module.params['description'], 362 'speed': module.params['speed'], 363 'mtu': module.params['mtu'], 364 'duplex': module.params['duplex'], 365 'state': module.params['state'], 366 'delay': module.params['delay'], 367 'tx_rate': module.params['tx_rate'], 368 'rx_rate': module.params['rx_rate'], 369 'neighbors': module.params['neighbors'] 370 } 371 372 validate_param_values(module, params) 373 if module.params['enabled']: 374 params.update({'disable': False}) 375 else: 376 params.update({'disable': True}) 377 378 obj.append(params) 379 return obj 380 381 382def map_obj_to_commands(updates): 383 commands = list() 384 want, have = updates 385 386 args = ('speed', 'description', 'duplex', 'mtu') 387 for w in want: 388 name = w['name'] 389 disable = w['disable'] 390 state = w['state'] 391 392 obj_in_have = search_obj_in_list(name, have) 393 interface = 'interface ' + name 394 if state == 'absent' and obj_in_have: 395 commands.append('no ' + interface) 396 elif state in ('present', 'up', 'down'): 397 if obj_in_have: 398 for item in args: 399 candidate = w.get(item) 400 running = obj_in_have.get(item) 401 if candidate != running: 402 if candidate: 403 cmd = item + ' ' + str(candidate) 404 add_command_to_interface(interface, cmd, commands) 405 406 if disable and not obj_in_have.get('disable', False): 407 add_command_to_interface(interface, 'shutdown', commands) 408 elif not disable and obj_in_have.get('disable', False): 409 add_command_to_interface(interface, 'no shutdown', commands) 410 else: 411 commands.append(interface) 412 for item in args: 413 value = w.get(item) 414 if value: 415 commands.append(item + ' ' + str(value)) 416 417 if disable: 418 commands.append('no shutdown') 419 return commands 420 421 422def check_declarative_intent_params(module, want, result): 423 failed_conditions = [] 424 have_neighbors_lldp = None 425 for w in want: 426 want_state = w.get('state') 427 want_tx_rate = w.get('tx_rate') 428 want_rx_rate = w.get('rx_rate') 429 want_neighbors = w.get('neighbors') 430 431 if want_state not in ('up', 'down') and not want_tx_rate and not want_rx_rate and not want_neighbors: 432 continue 433 434 if result['changed']: 435 sleep(w['delay']) 436 437 command = 'show interface %s brief' % w['name'] 438 rc, out, err = exec_command(module, command) 439 if rc != 0: 440 module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc) 441 if want_state in ('up', 'down'): 442 state_data = out.strip().lower().split(w['name']) 443 have_state = None 444 have_state = state_data[1].split()[3] 445 if have_state is None or not conditional(want_state, have_state.strip()): 446 failed_conditions.append('state ' + 'eq(%s)' % want_state) 447 448 command = 'show interface %s' % w['name'] 449 rc, out, err = exec_command(module, command) 450 have_tx_rate = None 451 have_rx_rate = None 452 rates = out.splitlines() 453 for s in rates: 454 s = s.strip() 455 if 'output rate' in s and 'input rate' in s: 456 sub = s.split() 457 if want_tx_rate: 458 have_tx_rate = sub[8] 459 if have_tx_rate is None or not conditional(want_tx_rate, have_tx_rate.strip(), cast=int): 460 failed_conditions.append('tx_rate ' + want_tx_rate) 461 if want_rx_rate: 462 have_rx_rate = sub[2] 463 if have_rx_rate is None or not conditional(want_rx_rate, have_rx_rate.strip(), cast=int): 464 failed_conditions.append('rx_rate ' + want_rx_rate) 465 if want_neighbors: 466 have_host = [] 467 have_port = [] 468 469 # Process LLDP neighbors 470 if have_neighbors_lldp is None: 471 rc, have_neighbors_lldp, err = exec_command(module, 'show lldp neighbors detail') 472 if rc != 0: 473 module.fail_json(msg=to_text(err, 474 errors='surrogate_then_replace'), 475 command=command, rc=rc) 476 477 if have_neighbors_lldp: 478 lines = have_neighbors_lldp.strip().split('Local Port ID: ') 479 for line in lines: 480 field = line.split('\n') 481 if field[0].strip() == w['name']: 482 for item in field: 483 if item.startswith('System Name:'): 484 have_host.append(item.split(':')[1].strip()) 485 if item.startswith('Port Description:'): 486 have_port.append(item.split(':')[1].strip()) 487 488 for item in want_neighbors: 489 host = item.get('host') 490 port = item.get('port') 491 if host and host not in have_host: 492 failed_conditions.append('host ' + host) 493 if port and port not in have_port: 494 failed_conditions.append('port ' + port) 495 return failed_conditions 496 497 498def main(): 499 """ main entry point for module execution 500 """ 501 neighbors_spec = dict( 502 host=dict(), 503 port=dict() 504 ) 505 506 element_spec = dict( 507 name=dict(), 508 description=dict(), 509 speed=dict(), 510 mtu=dict(), 511 duplex=dict(default='auto', choices=['full', 'half', 'auto']), 512 enabled=dict(default=True, type='bool'), 513 tx_rate=dict(), 514 rx_rate=dict(), 515 neighbors=dict(type='list', elements='dict', options=neighbors_spec), 516 delay=dict(default=20, type='int'), 517 state=dict(default='present', 518 choices=['present', 'absent', 'up', 'down']) 519 ) 520 521 aggregate_spec = deepcopy(element_spec) 522 aggregate_spec['name'] = dict(required=True) 523 524 # remove default in aggregate spec, to handle common arguments 525 remove_default_spec(aggregate_spec) 526 527 argument_spec = dict( 528 aggregate=dict(type='list', elements='dict', options=aggregate_spec), 529 ) 530 531 argument_spec.update(element_spec) 532 argument_spec.update(cnos_argument_spec) 533 534 required_one_of = [['name', 'aggregate']] 535 mutually_exclusive = [['name', 'aggregate']] 536 537 module = AnsibleModule(argument_spec=argument_spec, 538 required_one_of=required_one_of, 539 mutually_exclusive=mutually_exclusive, 540 supports_check_mode=True) 541 warnings = list() 542 check_args(module, warnings) 543 544 result = {'changed': False} 545 if warnings: 546 result['warnings'] = warnings 547 548 want = map_params_to_obj(module) 549 have = map_config_to_obj(module) 550 551 commands = map_obj_to_commands((want, have)) 552 result['commands'] = commands 553 554 if commands: 555 if not module.check_mode: 556 load_config(module, commands) 557 result['changed'] = True 558 559 failed_conditions = check_declarative_intent_params(module, want, result) 560 561 if failed_conditions: 562 msg = 'One or more conditional statements have not been satisfied' 563 module.fail_json(msg=msg, failed_conditions=failed_conditions) 564 565 module.exit_json(**result) 566 567 568if __name__ == '__main__': 569 main() 570