1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9ANSIBLE_METADATA = {'metadata_version': '1.1',
10                    'status': ['deprecated'],
11                    'supported_by': 'network'}
12
13DOCUMENTATION = """
14---
15module: nxos_interface
16extends_documentation_fragment: nxos
17version_added: "2.1"
18short_description: Manages physical attributes of interfaces.
19description:
20  - Manages physical attributes of interfaces of NX-OS switches.
21deprecated:
22  removed_in: '2.13'
23  alternative: nxos_interfaces
24  why: Updated modules released with more functionality
25author:
26  - Jason Edelman (@jedelman8)
27  - Trishna Guha (@trishnaguha)
28notes:
29  - Tested against NXOSv 7.3.(0)D1(1) on VIRL
30  - This module is also used to create logical interfaces such as
31    svis and loopbacks.
32  - Be cautious of platform specific idiosyncrasies. For example,
33    when you default a loopback interface, the admin state toggles
34    on certain versions of NX-OS.
35  - The M(nxos_overlay_global) C(anycast_gateway_mac) attribute must be
36    set before setting the C(fabric_forwarding_anycast_gateway) property.
37options:
38  name:
39    description:
40      - Full name of interface, i.e. Ethernet1/1, port-channel10.
41    required: true
42    aliases: [interface]
43  interface_type:
44    description:
45      - Interface type to be unconfigured from the device.
46    choices: ['loopback', 'portchannel', 'svi', 'nve']
47    version_added: 2.2
48  speed:
49    description:
50      - Interface link speed. Applicable for ethernet interface only.
51    version_added: 2.5
52  admin_state:
53    description:
54      - Administrative state of the interface.
55    default: up
56    choices: ['up','down']
57  description:
58    description:
59      - Interface description.
60  mode:
61    description:
62      - Manage Layer 2 or Layer 3 state of the interface.
63        This option is supported for ethernet and portchannel interface.
64        Applicable for ethernet and portchannel interface only.
65    choices: ['layer2','layer3']
66  mtu:
67    description:
68      - MTU for a specific interface. Must be an even number between 576 and 9216.
69        Applicable for ethernet interface only.
70    version_added: 2.5
71  ip_forward:
72    description:
73      - Enable/Disable ip forward feature on SVIs.
74    choices: ['enable','disable']
75    version_added: 2.2
76  fabric_forwarding_anycast_gateway:
77    description:
78      - Associate SVI with anycast gateway under VLAN configuration mode.
79        Applicable for SVI interface only.
80    type: bool
81    version_added: 2.2
82  duplex:
83    description:
84      - Interface link status. Applicable for ethernet interface only.
85    default: auto
86    choices: ['full', 'half', 'auto']
87    version_added: 2.5
88  tx_rate:
89    description:
90      - Transmit rate in bits per second (bps).
91      - This is state check parameter only.
92      - Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html)
93    version_added: 2.5
94  rx_rate:
95    description:
96      - Receiver rate in bits per second (bps).
97      - This is state check parameter only.
98      - Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html)
99    version_added: 2.5
100  neighbors:
101    description:
102      - Check the operational state of given interface C(name) for LLDP neighbor.
103      - The following suboptions are available. This is state check parameter only.
104    suboptions:
105        host:
106          description:
107            - "LLDP neighbor host for given interface C(name)."
108        port:
109          description:
110            - "LLDP neighbor port to which given interface C(name) is connected."
111    version_added: 2.5
112  aggregate:
113    description: List of Interfaces definitions.
114    version_added: 2.5
115  state:
116    description:
117      - Specify desired state of the resource.
118    default: present
119    choices: ['present','absent','default']
120  delay:
121    description:
122      - Time in seconds to wait before checking for the operational state on remote
123        device. This wait is applicable for operational state arguments.
124    default: 10
125"""
126
127EXAMPLES = """
128- name: Ensure an interface is a Layer 3 port and that it has the proper description
129  nxos_interface:
130    name: Ethernet1/1
131    description: 'Configured by Ansible'
132    mode: layer3
133
134- name: Admin down an interface
135  nxos_interface:
136    name: Ethernet2/1
137    admin_state: down
138
139- name: Remove all loopback interfaces
140  nxos_interface:
141    name: loopback
142    state: absent
143
144- name: Remove all logical interfaces
145  nxos_interface:
146    interface_type: "{{ item }} "
147    state: absent
148  loop:
149    - loopback
150    - portchannel
151    - svi
152    - nve
153
154- name: Admin up all loopback interfaces
155  nxos_interface:
156    name: loopback 0-1023
157    admin_state: up
158
159- name: Admin down all loopback interfaces
160  nxos_interface:
161    name: loopback 0-1023
162    admin_state: down
163
164- name: Check neighbors intent arguments
165  nxos_interface:
166    name: Ethernet2/3
167    neighbors:
168    - port: Ethernet2/3
169      host: abc.mycompany.com
170
171- name: Add interface using aggregate
172  nxos_interface:
173    aggregate:
174    - { name: Ethernet0/1, mtu: 256, description: test-interface-1 }
175    - { name: Ethernet0/2, mtu: 516, description: test-interface-2 }
176    duplex: full
177    speed: 100
178    state: present
179
180- name: Delete interface using aggregate
181  nxos_interface:
182    aggregate:
183    - name: Loopback9
184    - name: Loopback10
185    state: absent
186
187- name: Check intent arguments
188  nxos_interface:
189    name: Ethernet0/2
190    state: up
191    tx_rate: ge(0)
192    rx_rate: le(0)
193"""
194
195RETURN = """
196commands:
197    description: command list sent to the device
198    returned: always
199    type: list
200    sample:
201      - interface Ethernet2/3
202      - mtu 1500
203      - speed 10
204"""
205
206import re
207import time
208
209from copy import deepcopy
210
211from ansible.module_utils.network.nxos.nxos import load_config, run_commands
212from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, normalize_interface
213from ansible.module_utils.network.nxos.nxos import get_interface_type
214from ansible.module_utils.basic import AnsibleModule
215from ansible.module_utils.network.common.utils import conditional, remove_default_spec
216
217
218def execute_show_command(command, module):
219    if 'show run' not in command:
220        output = 'json'
221    else:
222        output = 'text'
223    cmds = [{
224        'command': command,
225        'output': output,
226    }]
227    body = run_commands(module, cmds, check_rc=False)
228    if body and "Invalid" in body[0]:
229        return []
230    else:
231        return body
232
233
234def search_obj_in_list(name, lst):
235    for o in lst:
236        if o['name'] == name:
237            return o
238
239    return None
240
241
242def get_interfaces_dict(module):
243    """Gets all active interfaces on a given switch
244    """
245    try:
246        body = execute_show_command('show interface', module)[0]
247    except IndexError:
248        return {}
249
250    interfaces = {
251        'ethernet': [],
252        'svi': [],
253        'loopback': [],
254        'management': [],
255        'portchannel': [],
256        'nve': [],
257        'unknown': []
258    }
259
260    if body:
261        interface_list = body['TABLE_interface']['ROW_interface']
262        for index in interface_list:
263            intf = index['interface']
264            intf_type = get_interface_type(intf)
265            interfaces[intf_type].append(intf)
266
267    return interfaces
268
269
270def get_vlan_interface_attributes(name, intf_type, module):
271    """ Returns dictionary that has two k/v pairs:
272        admin_state & description if not an svi, returns None
273    """
274    command = 'show run interface {0} all'.format(name)
275    try:
276        body = execute_show_command(command, module)[0]
277    except (IndexError, TypeError):
278        return None
279    if body:
280        command_list = body.split('\n')
281        desc = None
282        admin_state = 'down'
283        for each in command_list:
284            if 'description' in each:
285                desc = each.lstrip().split("description")[1].lstrip()
286            elif 'no shutdown' in each:
287                admin_state = 'up'
288        return dict(description=desc, admin_state=admin_state)
289    else:
290        return None
291
292
293def get_interface_type_removed_cmds(interfaces):
294    commands = []
295
296    for interface in interfaces:
297        if interface != 'Vlan1':
298            commands.append('no interface {0}'.format(interface))
299
300    return commands
301
302
303def get_admin_state(admin_state):
304    command = ''
305    if admin_state == 'up':
306        command = 'no shutdown'
307    elif admin_state == 'down':
308        command = 'shutdown'
309    return command
310
311
312def is_default_interface(name, module):
313    """Checks to see if interface exists and if it is a default config
314    """
315    command = 'show run interface {0}'.format(name)
316
317    try:
318        body = execute_show_command(command, module)[0]
319    except (IndexError, TypeError) as e:
320        body = ''
321
322    if body:
323        raw_list = body.split('\n')
324        found = False
325        for line in raw_list:
326            if line.startswith('interface'):
327                found = True
328            if found and line and not line.startswith('interface'):
329                return False
330        return True
331
332    else:
333        return 'DNE'
334
335
336def add_command_to_interface(interface, cmd, commands):
337    if interface not in commands:
338        commands.append(interface)
339    commands.append(cmd)
340
341
342def map_obj_to_commands(updates, module):
343    commands = list()
344    commands2 = list()
345    want, have = updates
346
347    args = ('speed', 'description', 'duplex', 'mtu')
348    for w in want:
349        name = w['name']
350        mode = w['mode']
351        ip_forward = w['ip_forward']
352        fabric_forwarding_anycast_gateway = w['fabric_forwarding_anycast_gateway']
353        admin_state = w['admin_state']
354        state = w['state']
355        interface_type = w['interface_type']
356        del w['state']
357        if name:
358            w['interface_type'] = None
359
360        if interface_type:
361            obj_in_have = {}
362            if state in ('present', 'default'):
363                module.fail_json(msg='The interface_type param can be used only with state absent.')
364        else:
365            obj_in_have = search_obj_in_list(name, have)
366            is_default = is_default_interface(name, module)
367
368        if name:
369            interface = 'interface ' + name
370
371        if state == 'absent':
372            if obj_in_have:
373                commands.append('no interface {0}'.format(name))
374            elif interface_type and not obj_in_have:
375                intfs = get_interfaces_dict(module)[interface_type]
376                cmds = get_interface_type_removed_cmds(intfs)
377                commands.extend(cmds)
378
379        elif state == 'present':
380            if obj_in_have:
381                # Don't run switchport command for loopback and svi interfaces
382                if get_interface_type(name) in ('ethernet', 'portchannel'):
383                    if mode == 'layer2' and mode != obj_in_have.get('mode'):
384                        add_command_to_interface(interface, 'switchport', commands)
385                    elif mode == 'layer3' and mode != obj_in_have.get('mode'):
386                        add_command_to_interface(interface, 'no switchport', commands)
387
388                if admin_state == 'up' and admin_state != obj_in_have.get('admin_state'):
389                    add_command_to_interface(interface, 'no shutdown', commands)
390                elif admin_state == 'down' and admin_state != obj_in_have.get('admin_state'):
391                    add_command_to_interface(interface, 'shutdown', commands)
392
393                if ip_forward == 'enable' and ip_forward != obj_in_have.get('ip_forward'):
394                    add_command_to_interface(interface, 'ip forward', commands)
395                elif ip_forward == 'disable' and ip_forward != obj_in_have.get('ip forward'):
396                    add_command_to_interface(interface, 'no ip forward', commands)
397
398                if (fabric_forwarding_anycast_gateway is True and
399                        obj_in_have.get('fabric_forwarding_anycast_gateway') is False):
400                    add_command_to_interface(interface, 'fabric forwarding mode anycast-gateway', commands)
401
402                elif (fabric_forwarding_anycast_gateway is False and
403                        obj_in_have.get('fabric_forwarding_anycast_gateway') is True):
404                    add_command_to_interface(interface, 'no fabric forwarding mode anycast-gateway', commands)
405
406                for item in args:
407                    candidate = w.get(item)
408                    if candidate and candidate != obj_in_have.get(item):
409                        cmd = item + ' ' + str(candidate)
410                        add_command_to_interface(interface, cmd, commands)
411
412                if name and get_interface_type(name) == 'ethernet':
413                    if mode != obj_in_have.get('mode'):
414                        admin_state = w.get('admin_state') or obj_in_have.get('admin_state')
415                        if admin_state:
416                            c1 = 'interface {0}'.format(normalize_interface(w['name']))
417                            c2 = get_admin_state(admin_state)
418                            commands2.append(c1)
419                            commands2.append(c2)
420
421            else:
422                commands.append(interface)
423                # Don't run switchport command for loopback and svi interfaces
424                if get_interface_type(name) in ('ethernet', 'portchannel'):
425                    if mode == 'layer2':
426                        commands.append('switchport')
427                    elif mode == 'layer3':
428                        commands.append('no switchport')
429
430                if admin_state == 'up':
431                    commands.append('no shutdown')
432                elif admin_state == 'down':
433                    commands.append('shutdown')
434
435                if ip_forward == 'enable':
436                    commands.append('ip forward')
437                elif ip_forward == 'disable':
438                    commands.append('no ip forward')
439
440                if fabric_forwarding_anycast_gateway is True:
441                    commands.append('fabric forwarding mode anycast-gateway')
442
443                elif fabric_forwarding_anycast_gateway is False:
444                    commands.append('no fabric forwarding mode anycast-gateway')
445
446                for item in args:
447                    candidate = w.get(item)
448                    if candidate:
449                        commands.append(item + ' ' + str(candidate))
450
451        elif state == 'default':
452            if is_default is False:
453                commands.append('default interface {0}'.format(name))
454            elif is_default == 'DNE':
455                module.exit_json(msg='interface you are trying to default does not exist')
456
457    return commands, commands2
458
459
460def map_params_to_obj(module):
461    obj = []
462    aggregate = module.params.get('aggregate')
463    if aggregate:
464        for item in aggregate:
465            for key in item:
466                if item.get(key) is None:
467                    item[key] = module.params[key]
468
469            d = item.copy()
470            name = d['name']
471            d['name'] = normalize_interface(name)
472            obj.append(d)
473
474    else:
475        obj.append({
476            'name': normalize_interface(module.params['name']),
477            'description': module.params['description'],
478            'speed': module.params['speed'],
479            'mode': module.params['mode'],
480            'mtu': module.params['mtu'],
481            'duplex': module.params['duplex'],
482            'ip_forward': module.params['ip_forward'],
483            'fabric_forwarding_anycast_gateway': module.params['fabric_forwarding_anycast_gateway'],
484            'admin_state': module.params['admin_state'],
485            'state': module.params['state'],
486            'interface_type': module.params['interface_type'],
487            'tx_rate': module.params['tx_rate'],
488            'rx_rate': module.params['rx_rate'],
489            'neighbors': module.params['neighbors']
490        })
491
492    return obj
493
494
495def map_config_to_obj(want, module):
496    objs = list()
497
498    for w in want:
499        obj = dict(name=None, description=None, admin_state=None, speed=None,
500                   mtu=None, mode=None, duplex=None, interface_type=None,
501                   ip_forward=None, fabric_forwarding_anycast_gateway=None)
502
503        if not w['name']:
504            return obj
505
506        command = 'show interface {0}'.format(w['name'])
507        try:
508            body = execute_show_command(command, module)[0]
509        except IndexError:
510            return list()
511        if body:
512            try:
513                interface_table = body['TABLE_interface']['ROW_interface']
514            except (KeyError, TypeError):
515                return list()
516
517            if interface_table:
518                if interface_table.get('eth_mode') == 'fex-fabric':
519                    module.fail_json(msg='nxos_interface does not support interfaces with mode "fex-fabric"')
520
521                intf_type = get_interface_type(w['name'])
522
523                if intf_type in ['portchannel', 'ethernet']:
524                    mode = interface_table.get('eth_mode')
525                    if mode in ('access', 'trunk', 'dot1q-tunnel'):
526                        obj['mode'] = 'layer2'
527                    elif mode in ('routed', 'layer3'):
528                        obj['mode'] = 'layer3'
529                    else:
530                        obj['mode'] = 'layer3'
531
532                if intf_type == 'ethernet':
533                    obj['name'] = normalize_interface(interface_table.get('interface'))
534                    obj['admin_state'] = interface_table.get('admin_state')
535                    obj['description'] = interface_table.get('desc')
536                    obj['mtu'] = interface_table.get('eth_mtu')
537                    obj['duplex'] = interface_table.get('eth_duplex')
538                    speed = interface_table.get('eth_speed')
539
540                    command = 'show run interface {0}'.format(obj['name'])
541                    body = execute_show_command(command, module)[0]
542
543                    speed_match = re.search(r'speed (\d+)', body)
544                    if speed_match is None:
545                        obj['speed'] = 'auto'
546                    else:
547                        obj['speed'] = speed_match.group(1)
548
549                    duplex_match = re.search(r'duplex (\S+)', body)
550                    if duplex_match is None:
551                        obj['duplex'] = 'auto'
552                    else:
553                        obj['duplex'] = duplex_match.group(1)
554
555                    if 'ip forward' in body:
556                        obj['ip_forward'] = 'enable'
557                    else:
558                        obj['ip_forward'] = 'disable'
559
560                elif intf_type == 'svi':
561                    obj['name'] = normalize_interface(interface_table.get('interface'))
562                    attributes = get_vlan_interface_attributes(obj['name'], intf_type, module)
563                    obj['admin_state'] = str(attributes.get('admin_state',
564                                                            'nxapibug'))
565                    obj['description'] = str(attributes.get('description',
566                                                            'nxapi_bug'))
567                    obj['mtu'] = interface_table.get('svi_mtu')
568
569                    command = 'show run interface {0}'.format(obj['name'])
570                    body = execute_show_command(command, module)[0]
571                    if 'ip forward' in body:
572                        obj['ip_forward'] = 'enable'
573                    else:
574                        obj['ip_forward'] = 'disable'
575                    if 'fabric forwarding mode anycast-gateway' in body:
576                        obj['fabric_forwarding_anycast_gateway'] = True
577                    else:
578                        obj['fabric_forwarding_anycast_gateway'] = False
579
580                elif intf_type in ('loopback', 'management', 'nve'):
581                    obj['name'] = normalize_interface(interface_table.get('interface'))
582                    obj['admin_state'] = interface_table.get('admin_state')
583                    if obj['admin_state'] is None and intf_type == 'loopback':
584                        # Some platforms don't have the 'admin_state' key.
585                        # For loopback interfaces it's safe to use the
586                        # 'state' key instead.
587                        obj['admin_state'] = interface_table.get('state')
588                    obj['description'] = interface_table.get('desc')
589
590                elif intf_type == 'portchannel':
591                    obj['name'] = normalize_interface(interface_table.get('interface'))
592                    obj['admin_state'] = interface_table.get('admin_state')
593                    obj['description'] = interface_table.get('desc')
594                    obj['mtu'] = interface_table.get('eth_mtu')
595
596                if obj['admin_state'] is None:
597                    # Some nxos platforms do not have the 'admin_state' key.
598                    # Use the 'state_rsn_desc' key instead to determine the
599                    # admin state of the interface.
600                    state_description = interface_table.get('state_rsn_desc')
601                    if state_description == 'Administratively down':
602                        obj['admin_state'] = 'down'
603                    elif state_description is not None:
604                        obj['admin_state'] = 'up'
605
606        objs.append(obj)
607
608    return objs
609
610
611def check_declarative_intent_params(module, want):
612    failed_conditions = []
613    have_neighbors = None
614    for w in want:
615        if w['interface_type']:
616            continue
617        want_tx_rate = w.get('tx_rate')
618        want_rx_rate = w.get('rx_rate')
619        want_neighbors = w.get('neighbors')
620        if not (want_tx_rate or want_rx_rate or want_neighbors):
621            continue
622
623        time.sleep(module.params['delay'])
624
625        cmd = [{'command': 'show interface {0}'.format(w['name']), 'output': 'text'}]
626
627        try:
628            out = run_commands(module, cmd, check_rc=False)[0]
629        except (AttributeError, IndexError, TypeError):
630            out = ''
631
632        if want_tx_rate:
633            match = re.search(r'output rate (\d+)', out, re.M)
634            have_tx_rate = None
635
636            if match:
637                have_tx_rate = match.group(1)
638
639            if have_tx_rate is None or not conditional(want_tx_rate, have_tx_rate.strip(), cast=int):
640                failed_conditions.append('tx_rate ' + want_tx_rate)
641
642        if want_rx_rate:
643            match = re.search(r'input rate (\d+)', out, re.M)
644            have_rx_rate = None
645
646            if match:
647                have_rx_rate = match.group(1)
648
649            if have_rx_rate is None or not conditional(want_rx_rate, have_rx_rate.strip(), cast=int):
650                failed_conditions.append('rx_rate ' + want_rx_rate)
651
652        if want_neighbors:
653            have_host = []
654            have_port = []
655            if have_neighbors is None:
656                cmd = [{'command': 'show lldp neighbors interface {0} detail'.format(w['name']), 'output': 'text'}]
657                output = run_commands(module, cmd, check_rc=False)
658                if output:
659                    have_neighbors = output[0]
660                else:
661                    have_neighbors = ''
662                if have_neighbors and 'Total entries displayed: 0' not in have_neighbors:
663                    for line in have_neighbors.strip().split('\n'):
664                        if line.startswith('Port Description'):
665                            have_port.append(line.split(': ')[1])
666                        if line.startswith('System Name'):
667                            have_host.append(line.split(': ')[1])
668
669            for item in want_neighbors:
670                host = item.get('host')
671                port = item.get('port')
672                if host and host not in have_host:
673                    failed_conditions.append('host ' + host)
674                if port and port not in have_port:
675                    failed_conditions.append('port ' + port)
676
677    return failed_conditions
678
679
680def main():
681    """ main entry point for module execution
682    """
683    neighbors_spec = dict(
684        host=dict(),
685        port=dict()
686    )
687
688    element_spec = dict(
689        name=dict(aliases=['interface']),
690        admin_state=dict(default='up', choices=['up', 'down']),
691        description=dict(),
692        speed=dict(),
693        mode=dict(choices=['layer2', 'layer3']),
694        mtu=dict(),
695        duplex=dict(choices=['full', 'half', 'auto']),
696        interface_type=dict(choices=['loopback', 'portchannel', 'svi', 'nve']),
697        ip_forward=dict(choices=['enable', 'disable']),
698        fabric_forwarding_anycast_gateway=dict(type='bool'),
699        tx_rate=dict(),
700        rx_rate=dict(),
701        neighbors=dict(type='list', elements='dict', options=neighbors_spec),
702        delay=dict(default=10, type='int'),
703        state=dict(choices=['absent', 'present', 'default'], default='present')
704    )
705
706    aggregate_spec = deepcopy(element_spec)
707    aggregate_spec['name'] = dict(required=True)
708
709    # remove default in aggregate spec, to handle common arguments
710    remove_default_spec(aggregate_spec)
711
712    argument_spec = dict(
713        aggregate=dict(type='list', elements='dict', options=aggregate_spec,
714                       mutually_exclusive=[['name', 'interface_type']])
715    )
716
717    argument_spec.update(element_spec)
718    argument_spec.update(nxos_argument_spec)
719
720    required_one_of = [['name', 'aggregate', 'interface_type']]
721    mutually_exclusive = [['name', 'aggregate'],
722                          ['name', 'interface_type']]
723
724    module = AnsibleModule(argument_spec=argument_spec,
725                           required_one_of=required_one_of,
726                           mutually_exclusive=mutually_exclusive,
727                           supports_check_mode=True)
728    warnings = list()
729
730    result = {'changed': False}
731    if warnings:
732        result['warnings'] = warnings
733
734    want = map_params_to_obj(module)
735    have = map_config_to_obj(want, module)
736
737    commands = []
738    commands1, commands2 = map_obj_to_commands((want, have), module)
739    commands.extend(commands1)
740
741    if commands:
742        if not module.check_mode:
743            load_config(module, commands)
744            result['changed'] = True
745            # if the mode changes from L2 to L3, the admin state
746            # seems to change after the API call, so adding a second API
747            # call to ensure it's in the desired state.
748            if commands2:
749                load_config(module, commands2)
750                commands.extend(commands2)
751            commands = [cmd for cmd in commands if cmd != 'configure']
752    result['commands'] = commands
753
754    if result['changed']:
755        failed_conditions = check_declarative_intent_params(module, want)
756
757        if failed_conditions:
758            msg = 'One or more conditional statements have not been satisfied'
759            module.fail_json(msg=msg, failed_conditions=failed_conditions)
760
761    module.exit_json(**result)
762
763
764if __name__ == '__main__':
765    main()
766