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
15DOCUMENTATION = """
16---
17module: ios_vlan
18version_added: "2.5"
19author: "Trishna Guha (@trishnaguha)"
20short_description: Manage VLANs on IOS network devices
21description:
22  - This module provides declarative management of VLANs
23    on Cisco IOS network devices.
24deprecated:
25  removed_in: '2.13'
26  alternative: ios_vlans
27  why: Newer and updated modules released with more functionality in Ansible 2.9
28notes:
29  - Tested against IOS 15.2
30options:
31  name:
32    description:
33      - Name of the VLAN.
34  vlan_id:
35    description:
36      - ID of the VLAN. Range 1-4094.
37    required: true
38  interfaces:
39    description:
40      - List of interfaces that should be associated to the VLAN.
41    required: true
42  associated_interfaces:
43    description:
44      - This is a intent option and checks the operational state of the for given vlan C(name)
45        for associated interfaces. If the value in the C(associated_interfaces) does not match with
46        the operational state of vlan interfaces on device it will result in failure.
47    version_added: "2.5"
48  delay:
49    description:
50      - Delay the play should wait to check for declarative intent params values.
51    default: 10
52  aggregate:
53    description: List of VLANs definitions.
54  purge:
55    description:
56      - Purge VLANs not defined in the I(aggregate) parameter.
57    default: no
58    type: bool
59  state:
60    description:
61      - State of the VLAN configuration.
62    default: present
63    choices: ['present', 'absent', 'active', 'suspend']
64extends_documentation_fragment: ios
65"""
66
67EXAMPLES = """
68- name: Create vlan
69  ios_vlan:
70    vlan_id: 100
71    name: test-vlan
72    state: present
73
74- name: Add interfaces to VLAN
75  ios_vlan:
76    vlan_id: 100
77    interfaces:
78      - GigabitEthernet0/0
79      - GigabitEthernet0/1
80
81- name: Check if interfaces is assigned to VLAN
82  ios_vlan:
83    vlan_id: 100
84    associated_interfaces:
85      - GigabitEthernet0/0
86      - GigabitEthernet0/1
87
88- name: Delete vlan
89  ios_vlan:
90    vlan_id: 100
91    state: absent
92
93- name: Add vlan using aggregate
94  ios_vlan:
95    aggregate:
96    - { vlan_id: 100, name: test-vlan, interfaces: [GigabitEthernet0/1, GigabitEthernet0/2], delay: 15, state: suspend }
97    - { vlan_id: 101, name: test-vlan, interfaces: GigabitEthernet0/3 }
98
99- name: Move interfaces to a different VLAN
100  ios_vlan:
101    vlan_id: 102
102    interfaces:
103      - GigabitEthernet0/0
104      - GigabitEthernet0/1
105"""
106
107RETURN = """
108commands:
109  description: The list of configuration mode commands to send to the device
110  returned: always
111  type: list
112  sample:
113    - vlan 100
114    - name test-vlan
115"""
116
117import re
118import time
119
120from copy import deepcopy
121
122from ansible.module_utils.basic import AnsibleModule
123from ansible.module_utils.network.common.utils import remove_default_spec
124from ansible.module_utils.network.ios.ios import load_config, run_commands, normalize_interface
125from ansible.module_utils.network.ios.ios import ios_argument_spec
126
127
128def search_obj_in_list(vlan_id, lst):
129    for o in lst:
130        if o['vlan_id'] == vlan_id:
131            return o
132
133
134def map_obj_to_commands(updates, module):
135    commands = list()
136    want, have = updates
137    purge = module.params['purge']
138
139    for w in want:
140        vlan_id = w['vlan_id']
141        name = w['name']
142        interfaces = w['interfaces']
143        state = w['state']
144
145        obj_in_have = search_obj_in_list(vlan_id, have)
146
147        if state == 'absent':
148            if obj_in_have:
149                commands.append('no vlan {0}'.format(vlan_id))
150
151        elif state == 'present':
152            if not obj_in_have:
153                commands.append('vlan {0}'.format(vlan_id))
154                if name:
155                    commands.append('name {0}'.format(name))
156
157                if interfaces:
158                    for i in interfaces:
159                        commands.append('interface {0}'.format(i))
160                        commands.append('switchport mode access')
161                        commands.append('switchport access vlan {0}'.format(vlan_id))
162
163            else:
164                if name:
165                    if name != obj_in_have['name']:
166                        commands.append('vlan {0}'.format(vlan_id))
167                        commands.append('name {0}'.format(name))
168
169                if interfaces:
170                    if not obj_in_have['interfaces']:
171                        for i in interfaces:
172                            commands.append('vlan {0}'.format(vlan_id))
173                            commands.append('interface {0}'.format(i))
174                            commands.append('switchport mode access')
175                            commands.append('switchport access vlan {0}'.format(vlan_id))
176
177                    elif set(interfaces) != set(obj_in_have['interfaces']):
178                        missing_interfaces = list(set(interfaces) - set(obj_in_have['interfaces']))
179                        for i in missing_interfaces:
180                            commands.append('vlan {0}'.format(vlan_id))
181                            commands.append('interface {0}'.format(i))
182                            commands.append('switchport mode access')
183                            commands.append('switchport access vlan {0}'.format(vlan_id))
184
185                        superfluous_interfaces = list(set(obj_in_have['interfaces']) - set(interfaces))
186                        for i in superfluous_interfaces:
187                            commands.append('vlan {0}'.format(vlan_id))
188                            commands.append('interface {0}'.format(i))
189                            commands.append('switchport mode access')
190                            commands.append('no switchport access vlan {0}'.format(vlan_id))
191        else:
192            commands.append('vlan {0}'.format(vlan_id))
193            if name:
194                commands.append('name {0}'.format(name))
195            commands.append('state {0}'.format(state))
196
197    if purge:
198        for h in have:
199            obj_in_want = search_obj_in_list(h['vlan_id'], want)
200            if not obj_in_want and h['vlan_id'] != '1':
201                commands.append('no vlan {0}'.format(h['vlan_id']))
202
203    return commands
204
205
206def map_params_to_obj(module):
207    obj = []
208    aggregate = module.params.get('aggregate')
209    if aggregate:
210        for item in aggregate:
211            for key in item:
212                if item.get(key) is None:
213                    item[key] = module.params[key]
214
215            d = item.copy()
216            d['vlan_id'] = str(d['vlan_id'])
217
218            obj.append(d)
219    else:
220        obj.append({
221            'vlan_id': str(module.params['vlan_id']),
222            'name': module.params['name'],
223            'interfaces': module.params['interfaces'],
224            'associated_interfaces': module.params['associated_interfaces'],
225            'state': module.params['state']
226        })
227
228    return obj
229
230
231def parse_to_logical_rows(out):
232    started_yielding = False
233    cur_row = []
234    for l in out.splitlines()[2:]:
235        if not l:
236            """Skip empty lines."""
237            continue
238        if '0' < l[0] <= '9':
239            """Line starting with a number."""
240            if started_yielding:
241                yield cur_row
242                cur_row = []  # Reset it to hold a next chunk
243            started_yielding = True
244        cur_row.append(l)
245
246    # Return the rest of it:
247    yield cur_row
248
249
250def map_ports_str_to_list(ports_str):
251    return list(filter(bool, (normalize_interface(p.strip()) for p in ports_str.split(', '))))
252
253
254def parse_to_obj(logical_rows):
255    first_row = logical_rows[0]
256    rest_rows = logical_rows[1:]
257    obj = re.match(r'(?P<vlan_id>\d+)\s+(?P<name>[^\s]+)\s+(?P<state>[^\s]+)\s*(?P<interfaces>.*)', first_row).groupdict()
258    if obj['state'] == 'suspended':
259        obj['state'] = 'suspend'
260    obj['interfaces'] = map_ports_str_to_list(obj['interfaces'])
261    obj['interfaces'].extend(prts_r for prts in rest_rows for prts_r in map_ports_str_to_list(prts))
262    return obj
263
264
265def parse_vlan_brief(vlan_out):
266    return [parse_to_obj(r) for r in parse_to_logical_rows(vlan_out)]
267
268
269def map_config_to_obj(module):
270    return parse_vlan_brief(run_commands(module, ['show vlan brief'])[0])
271
272
273def check_declarative_intent_params(want, module, result):
274
275    have = None
276    is_delay = False
277
278    for w in want:
279        if w.get('associated_interfaces') is None:
280            continue
281
282        if result['changed'] and not is_delay:
283            time.sleep(module.params['delay'])
284            is_delay = True
285
286        if have is None:
287            have = map_config_to_obj(module)
288
289        for i in w['associated_interfaces']:
290            obj_in_have = search_obj_in_list(w['vlan_id'], have)
291            if obj_in_have and 'interfaces' in obj_in_have and i not in obj_in_have['interfaces']:
292                module.fail_json(msg="Interface %s not configured on vlan %s" % (i, w['vlan_id']))
293
294
295def main():
296    """ main entry point for module execution
297    """
298    element_spec = dict(
299        vlan_id=dict(type='int'),
300        name=dict(),
301        interfaces=dict(type='list'),
302        associated_interfaces=dict(type='list'),
303        delay=dict(default=10, type='int'),
304        state=dict(default='present',
305                   choices=['present', 'absent', 'active', 'suspend'])
306    )
307
308    aggregate_spec = deepcopy(element_spec)
309    aggregate_spec['vlan_id'] = dict(required=True)
310
311    # remove default in aggregate spec, to handle common arguments
312    remove_default_spec(aggregate_spec)
313
314    argument_spec = dict(
315        aggregate=dict(type='list', elements='dict', options=aggregate_spec),
316        purge=dict(default=False, type='bool')
317    )
318
319    argument_spec.update(element_spec)
320    argument_spec.update(ios_argument_spec)
321
322    required_one_of = [['vlan_id', 'aggregate']]
323    mutually_exclusive = [['vlan_id', 'aggregate']]
324
325    module = AnsibleModule(argument_spec=argument_spec,
326                           required_one_of=required_one_of,
327                           mutually_exclusive=mutually_exclusive,
328                           supports_check_mode=True)
329    warnings = list()
330    result = {'changed': False}
331    if warnings:
332        result['warnings'] = warnings
333
334    want = map_params_to_obj(module)
335    have = map_config_to_obj(module)
336    commands = map_obj_to_commands((want, have), module)
337    result['commands'] = commands
338
339    if commands:
340        if not module.check_mode:
341            load_config(module, commands)
342        result['changed'] = True
343
344    check_declarative_intent_params(want, module, result)
345
346    module.exit_json(**result)
347
348
349if __name__ == '__main__':
350    main()
351