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