1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# (c) 2017, Ansible by Red Hat, inc
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['deprecated'],
13                    'supported_by': 'network'}
14
15
16DOCUMENTATION = """
17---
18module: ios_interface
19version_added: "2.4"
20author: "Ganesh Nalawade (@ganeshrn)"
21short_description: Manage Interface on Cisco IOS network devices
22description:
23  - This module provides declarative management of Interfaces
24    on Cisco IOS network devices.
25deprecated:
26  removed_in: '2.13'
27  alternative: ios_interfaces
28  why: Newer and updated modules released with more functionality in Ansible 2.9
29notes:
30  - Tested against IOS 15.6
31options:
32  name:
33    description:
34      - Name of the Interface.
35    required: true
36  description:
37    description:
38      - Description of Interface.
39  enabled:
40    description:
41      - Interface link status.
42    type: bool
43  speed:
44    description:
45      - Interface link speed.
46  mtu:
47    description:
48      - Maximum size of transmit packet.
49  duplex:
50    description:
51      - Interface link status
52    default: auto
53    choices: ['full', 'half', 'auto']
54  tx_rate:
55    description:
56      - Transmit rate in bits per second (bps).
57      - This is state check parameter only.
58      - Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html)
59  rx_rate:
60    description:
61      - Receiver rate in bits per second (bps).
62      - This is state check parameter only.
63      - Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html)
64  neighbors:
65    description:
66      - Check the operational state of given interface C(name) for CDP/LLDP neighbor.
67      - The following suboptions are available.
68    suboptions:
69        host:
70          description:
71            - "CDP/LLDP neighbor host for given interface C(name)."
72        port:
73          description:
74            - "CDP/LLDP neighbor port to which given interface C(name) is connected."
75  aggregate:
76    description: List of Interfaces definitions.
77  delay:
78    description:
79      - Time in seconds to wait before checking for the operational state on remote
80        device. This wait is applicable for operational state argument which are
81        I(state) with values C(up)/C(down), I(tx_rate) and I(rx_rate).
82    default: 10
83  state:
84    description:
85      - State of the Interface configuration, C(up) means present and
86        operationally up and C(down) means present and operationally C(down)
87    default: present
88    choices: ['present', 'absent', 'up', 'down']
89extends_documentation_fragment: ios
90"""
91
92EXAMPLES = """
93- name: configure interface
94  ios_interface:
95      name: GigabitEthernet0/2
96      description: test-interface
97      speed: 100
98      duplex: half
99      mtu: 512
100
101- name: remove interface
102  ios_interface:
103    name: Loopback9
104    state: absent
105
106- name: make interface up
107  ios_interface:
108    name: GigabitEthernet0/2
109    enabled: True
110
111- name: make interface down
112  ios_interface:
113    name: GigabitEthernet0/2
114    enabled: False
115
116- name: Check intent arguments
117  ios_interface:
118    name: GigabitEthernet0/2
119    state: up
120    tx_rate: ge(0)
121    rx_rate: le(0)
122
123- name: Check neighbors intent arguments
124  ios_interface:
125    name: Gi0/0
126    neighbors:
127    - port: eth0
128      host: netdev
129
130- name: Config + intent
131  ios_interface:
132    name: GigabitEthernet0/2
133    enabled: False
134    state: down
135
136- name: Add interface using aggregate
137  ios_interface:
138    aggregate:
139    - { name: GigabitEthernet0/1, mtu: 256, description: test-interface-1 }
140    - { name: GigabitEthernet0/2, mtu: 516, description: test-interface-2 }
141    duplex: full
142    speed: 100
143    state: present
144
145- name: Delete interface using aggregate
146  ios_interface:
147    aggregate:
148    - name: Loopback9
149    - name: Loopback10
150    state: absent
151"""
152
153RETURN = """
154commands:
155  description: The list of configuration mode commands to send to the device.
156  returned: always, except for the platforms that use Netconf transport to manage the device.
157  type: list
158  sample:
159  - interface GigabitEthernet0/2
160  - description test-interface
161  - duplex half
162  - mtu 512
163"""
164import re
165
166from copy import deepcopy
167from time import sleep
168
169from ansible.module_utils._text import to_text
170from ansible.module_utils.basic import AnsibleModule
171from ansible.module_utils.connection import exec_command
172from ansible.module_utils.network.ios.ios import get_config, load_config
173from ansible.module_utils.network.ios.ios import ios_argument_spec, check_args
174from ansible.module_utils.network.common.config import NetworkConfig
175from ansible.module_utils.network.common.utils import conditional, remove_default_spec
176
177
178def validate_mtu(value, module):
179    if value and not 64 <= int(value) <= 9600:
180        module.fail_json(msg='mtu must be between 64 and 9600')
181
182
183def validate_param_values(module, obj, param=None):
184    if param is None:
185        param = module.params
186    for key in obj:
187        # validate the param value (if validator func exists)
188        validator = globals().get('validate_%s' % key)
189        if callable(validator):
190            validator(param.get(key), module)
191
192
193def parse_shutdown(configobj, name):
194    cfg = configobj['interface %s' % name]
195    cfg = '\n'.join(cfg.children)
196    match = re.search(r'^shutdown', cfg, re.M)
197    if match:
198        return True
199    else:
200        return False
201
202
203def parse_config_argument(configobj, name, arg=None):
204    cfg = configobj['interface %s' % name]
205    cfg = '\n'.join(cfg.children)
206    match = re.search(r'%s (.+)$' % arg, cfg, re.M)
207    if match:
208        return match.group(1)
209
210
211def search_obj_in_list(name, lst):
212    for o in lst:
213        if o['name'] == name:
214            return o
215
216    return None
217
218
219def add_command_to_interface(interface, cmd, commands):
220    if interface not in commands:
221        commands.append(interface)
222    commands.append(cmd)
223
224
225def map_config_to_obj(module):
226    config = get_config(module)
227    configobj = NetworkConfig(indent=1, contents=config)
228
229    match = re.findall(r'^interface (\S+)', config, re.M)
230    if not match:
231        return list()
232
233    instances = list()
234
235    for item in set(match):
236        obj = {
237            'name': item,
238            'description': parse_config_argument(configobj, item, 'description'),
239            'speed': parse_config_argument(configobj, item, 'speed'),
240            'duplex': parse_config_argument(configobj, item, 'duplex'),
241            'mtu': parse_config_argument(configobj, item, 'mtu'),
242            'disable': True if parse_shutdown(configobj, item) else False,
243            'state': 'present'
244        }
245        instances.append(obj)
246    return instances
247
248
249def map_params_to_obj(module):
250    obj = []
251    aggregate = module.params.get('aggregate')
252    if aggregate:
253        for item in aggregate:
254            for key in item:
255                if item.get(key) is None:
256                    item[key] = module.params[key]
257
258            validate_param_values(module, item, item)
259            d = item.copy()
260
261            if d['enabled']:
262                d['disable'] = False
263            else:
264                d['disable'] = True
265
266            obj.append(d)
267
268    else:
269        params = {
270            'name': module.params['name'],
271            'description': module.params['description'],
272            'speed': module.params['speed'],
273            'mtu': module.params['mtu'],
274            'duplex': module.params['duplex'],
275            'state': module.params['state'],
276            'delay': module.params['delay'],
277            'tx_rate': module.params['tx_rate'],
278            'rx_rate': module.params['rx_rate'],
279            'neighbors': module.params['neighbors']
280        }
281
282        validate_param_values(module, params)
283        if module.params['enabled']:
284            params.update({'disable': False})
285        else:
286            params.update({'disable': True})
287
288        obj.append(params)
289    return obj
290
291
292def map_obj_to_commands(updates):
293    commands = list()
294    want, have = updates
295
296    args = ('speed', 'description', 'duplex', 'mtu')
297    for w in want:
298        name = w['name']
299        disable = w['disable']
300        state = w['state']
301
302        obj_in_have = search_obj_in_list(name, have)
303        interface = 'interface ' + name
304
305        if state == 'absent' and obj_in_have:
306            commands.append('no ' + interface)
307
308        elif state in ('present', 'up', 'down'):
309            if obj_in_have:
310                for item in args:
311                    candidate = w.get(item)
312                    running = obj_in_have.get(item)
313                    if candidate != running:
314                        if candidate:
315                            cmd = item + ' ' + str(candidate)
316                            add_command_to_interface(interface, cmd, commands)
317
318                if disable and not obj_in_have.get('disable', False):
319                    add_command_to_interface(interface, 'shutdown', commands)
320                elif not disable and obj_in_have.get('disable', False):
321                    add_command_to_interface(interface, 'no shutdown', commands)
322            else:
323                commands.append(interface)
324                for item in args:
325                    value = w.get(item)
326                    if value:
327                        commands.append(item + ' ' + str(value))
328
329                if disable:
330                    commands.append('no shutdown')
331    return commands
332
333
334def check_declarative_intent_params(module, want, result):
335    failed_conditions = []
336    have_neighbors_lldp = None
337    have_neighbors_cdp = None
338    for w in want:
339        want_state = w.get('state')
340        want_tx_rate = w.get('tx_rate')
341        want_rx_rate = w.get('rx_rate')
342        want_neighbors = w.get('neighbors')
343
344        if want_state not in ('up', 'down') and not want_tx_rate and not want_rx_rate and not want_neighbors:
345            continue
346
347        if result['changed']:
348            sleep(w['delay'])
349
350        command = 'show interfaces %s' % w['name']
351        rc, out, err = exec_command(module, command)
352        if rc != 0:
353            module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc)
354
355        if want_state in ('up', 'down'):
356            match = re.search(r'%s (\w+)' % 'line protocol is', out, re.M)
357            have_state = None
358            if match:
359                have_state = match.group(1)
360            if have_state is None or not conditional(want_state, have_state.strip()):
361                failed_conditions.append('state ' + 'eq(%s)' % want_state)
362
363        if want_tx_rate:
364            match = re.search(r'%s (\d+)' % 'output rate', out, re.M)
365            have_tx_rate = None
366            if match:
367                have_tx_rate = match.group(1)
368
369            if have_tx_rate is None or not conditional(want_tx_rate, have_tx_rate.strip(), cast=int):
370                failed_conditions.append('tx_rate ' + want_tx_rate)
371
372        if want_rx_rate:
373            match = re.search(r'%s (\d+)' % 'input rate', out, re.M)
374            have_rx_rate = None
375            if match:
376                have_rx_rate = match.group(1)
377
378            if have_rx_rate is None or not conditional(want_rx_rate, have_rx_rate.strip(), cast=int):
379                failed_conditions.append('rx_rate ' + want_rx_rate)
380
381        if want_neighbors:
382            have_host = []
383            have_port = []
384
385            # Process LLDP neighbors
386            if have_neighbors_lldp is None:
387                rc, have_neighbors_lldp, err = exec_command(module, 'show lldp neighbors detail')
388                if rc != 0:
389                    module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc)
390
391            if have_neighbors_lldp:
392                lines = have_neighbors_lldp.strip().split('Local Intf: ')
393                for line in lines:
394                    field = line.split('\n')
395                    if field[0].strip() == w['name']:
396                        for item in field:
397                            if item.startswith('System Name:'):
398                                have_host.append(item.split(':')[1].strip())
399                            if item.startswith('Port Description:'):
400                                have_port.append(item.split(':')[1].strip())
401
402            # Process CDP neighbors
403            if have_neighbors_cdp is None:
404                rc, have_neighbors_cdp, err = exec_command(module, 'show cdp neighbors detail')
405                if rc != 0:
406                    module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc)
407
408            if have_neighbors_cdp:
409                neighbors_cdp = re.findall('Device ID: (.*?)\n.*?Interface: (.*?),  Port ID .outgoing port.: (.*?)\n', have_neighbors_cdp, re.S)
410                for host, localif, remoteif in neighbors_cdp:
411                    if localif == w['name']:
412                        have_host.append(host)
413                        have_port.append(remoteif)
414
415            for item in want_neighbors:
416                host = item.get('host')
417                port = item.get('port')
418                if host and host not in have_host:
419                    failed_conditions.append('host ' + host)
420                if port and port not in have_port:
421                    failed_conditions.append('port ' + port)
422    return failed_conditions
423
424
425def main():
426    """ main entry point for module execution
427    """
428    neighbors_spec = dict(
429        host=dict(),
430        port=dict()
431    )
432
433    element_spec = dict(
434        name=dict(),
435        description=dict(),
436        speed=dict(),
437        mtu=dict(),
438        duplex=dict(choices=['full', 'half', 'auto']),
439        enabled=dict(default=True, type='bool'),
440        tx_rate=dict(),
441        rx_rate=dict(),
442        neighbors=dict(type='list', elements='dict', options=neighbors_spec),
443        delay=dict(default=10, type='int'),
444        state=dict(default='present',
445                   choices=['present', 'absent', 'up', 'down'])
446    )
447
448    aggregate_spec = deepcopy(element_spec)
449    aggregate_spec['name'] = dict(required=True)
450
451    # remove default in aggregate spec, to handle common arguments
452    remove_default_spec(aggregate_spec)
453
454    argument_spec = dict(
455        aggregate=dict(type='list', elements='dict', options=aggregate_spec),
456    )
457
458    argument_spec.update(element_spec)
459    argument_spec.update(ios_argument_spec)
460
461    required_one_of = [['name', 'aggregate']]
462    mutually_exclusive = [['name', 'aggregate']]
463
464    module = AnsibleModule(argument_spec=argument_spec,
465                           required_one_of=required_one_of,
466                           mutually_exclusive=mutually_exclusive,
467                           supports_check_mode=True)
468    warnings = list()
469    check_args(module, warnings)
470
471    result = {'changed': False}
472    if warnings:
473        result['warnings'] = warnings
474
475    want = map_params_to_obj(module)
476    have = map_config_to_obj(module)
477
478    commands = map_obj_to_commands((want, have))
479    result['commands'] = commands
480
481    if commands:
482        if not module.check_mode:
483            load_config(module, commands)
484        result['changed'] = True
485
486    failed_conditions = check_declarative_intent_params(module, want, result)
487
488    if failed_conditions:
489        msg = 'One or more conditional statements have not been satisfied'
490        module.fail_json(msg=msg, failed_conditions=failed_conditions)
491
492    module.exit_json(**result)
493
494
495if __name__ == '__main__':
496    main()
497