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': ['preview'],
13                    'supported_by': 'network'}
14
15DOCUMENTATION = """
16---
17module: ios_linkagg
18version_added: "2.5"
19author: "Trishna Guha (@trishnaguha)"
20short_description: Manage link aggregation groups on Cisco IOS network devices
21description:
22  - This module provides declarative management of link aggregation groups
23    on Cisco IOS network devices.
24notes:
25  - Tested against IOS 15.2
26options:
27  group:
28    description:
29      - Channel-group number for the port-channel
30        Link aggregation group. Range 1-255.
31  mode:
32    description:
33      - Mode of the link aggregation group.
34    choices: ['active', 'on', 'passive', 'auto', 'desirable']
35  members:
36    description:
37      - List of members of the link aggregation group.
38  aggregate:
39    description: List of link aggregation definitions.
40  state:
41    description:
42      - State of the link aggregation group.
43    default: present
44    choices: ['present', 'absent']
45  purge:
46    description:
47      - Purge links not defined in the I(aggregate) parameter.
48    default: no
49    type: bool
50extends_documentation_fragment: ios
51"""
52
53EXAMPLES = """
54- name: create link aggregation group
55  ios_linkagg:
56    group: 10
57    state: present
58
59- name: delete link aggregation group
60  ios_linkagg:
61    group: 10
62    state: absent
63
64- name: set link aggregation group to members
65  ios_linkagg:
66    group: 200
67    mode: active
68    members:
69      - GigabitEthernet0/0
70      - GigabitEthernet0/1
71
72- name: remove link aggregation group from GigabitEthernet0/0
73  ios_linkagg:
74    group: 200
75    mode: active
76    members:
77      - GigabitEthernet0/1
78
79- name: Create aggregate of linkagg definitions
80  ios_linkagg:
81    aggregate:
82      - { group: 3, mode: on, members: [GigabitEthernet0/1] }
83      - { group: 100, mode: passive, members: [GigabitEthernet0/2] }
84"""
85
86RETURN = """
87commands:
88  description: The list of configuration mode commands to send to the device
89  returned: always, except for the platforms that use Netconf transport to manage the device.
90  type: list
91  sample:
92    - interface port-channel 30
93    - interface GigabitEthernet0/3
94    - channel-group 30 mode on
95    - no interface port-channel 30
96"""
97
98import re
99from copy import deepcopy
100
101from ansible.module_utils.basic import AnsibleModule
102from ansible.module_utils.network.common.config import CustomNetworkConfig
103from ansible.module_utils.network.common.utils import remove_default_spec
104from ansible.module_utils.network.ios.ios import get_config, load_config
105from ansible.module_utils.network.ios.ios import ios_argument_spec
106
107
108def search_obj_in_list(group, lst):
109    for o in lst:
110        if o['group'] == group:
111            return o
112
113
114def map_obj_to_commands(updates, module):
115    commands = list()
116    want, have = updates
117    purge = module.params['purge']
118
119    for w in want:
120        group = w['group']
121        mode = w['mode']
122        members = w.get('members') or []
123        state = w['state']
124        del w['state']
125
126        obj_in_have = search_obj_in_list(group, have)
127
128        if state == 'absent':
129            if obj_in_have:
130                commands.append('no interface port-channel {0}'.format(group))
131
132        elif state == 'present':
133            cmd = ['interface port-channel {0}'.format(group),
134                   'end']
135            if not obj_in_have:
136                if not group:
137                    module.fail_json(msg='group is a required option')
138                commands.extend(cmd)
139
140                if members:
141                    for m in members:
142                        commands.append('interface {0}'.format(m))
143                        commands.append('channel-group {0} mode {1}'.format(group, mode))
144
145            else:
146                if members:
147                    if 'members' not in obj_in_have.keys():
148                        for m in members:
149                            commands.extend(cmd)
150                            commands.append('interface {0}'.format(m))
151                            commands.append('channel-group {0} mode {1}'.format(group, mode))
152
153                    elif set(members) != set(obj_in_have['members']):
154                        missing_members = list(set(members) - set(obj_in_have['members']))
155                        for m in missing_members:
156                            commands.extend(cmd)
157                            commands.append('interface {0}'.format(m))
158                            commands.append('channel-group {0} mode {1}'.format(group, mode))
159
160                        superfluous_members = list(set(obj_in_have['members']) - set(members))
161                        for m in superfluous_members:
162                            commands.extend(cmd)
163                            commands.append('interface {0}'.format(m))
164                            commands.append('no channel-group {0} mode {1}'.format(group, mode))
165
166    if purge:
167        for h in have:
168            obj_in_want = search_obj_in_list(h['group'], want)
169            if not obj_in_want:
170                commands.append('no interface port-channel {0}'.format(h['group']))
171
172    return commands
173
174
175def map_params_to_obj(module):
176    obj = []
177
178    aggregate = module.params.get('aggregate')
179    if aggregate:
180        for item in aggregate:
181            for key in item:
182                if item.get(key) is None:
183                    item[key] = module.params[key]
184
185            d = item.copy()
186            d['group'] = str(d['group'])
187
188            obj.append(d)
189    else:
190        obj.append({
191            'group': str(module.params['group']),
192            'mode': module.params['mode'],
193            'members': module.params['members'],
194            'state': module.params['state']
195        })
196
197    return obj
198
199
200def parse_mode(module, config, group, member):
201    mode = None
202    netcfg = CustomNetworkConfig(indent=1, contents=config)
203    parents = ['interface {0}'.format(member)]
204    body = netcfg.get_section(parents)
205
206    match_int = re.findall(r'interface {0}\n'.format(member), body, re.M)
207    if match_int:
208        match = re.search(r'channel-group {0} mode (\S+)'.format(group), body, re.M)
209        if match:
210            mode = match.group(1)
211
212    return mode
213
214
215def parse_members(module, config, group):
216    members = []
217
218    for line in config.strip().split('!'):
219        l = line.strip()
220        if l.startswith('interface'):
221            match_group = re.findall(r'channel-group {0} mode'.format(group), l, re.M)
222            if match_group:
223                match = re.search(r'interface (\S+)', l, re.M)
224                if match:
225                    members.append(match.group(1))
226
227    return members
228
229
230def get_channel(module, config, group):
231    match = re.findall(r'^interface (\S+)', config, re.M)
232
233    if not match:
234        return {}
235
236    channel = {}
237    for item in set(match):
238        member = item
239        channel['mode'] = parse_mode(module, config, group, member)
240        channel['members'] = parse_members(module, config, group)
241
242    return channel
243
244
245def map_config_to_obj(module):
246    objs = list()
247    config = get_config(module)
248
249    for line in config.split('\n'):
250        l = line.strip()
251        match = re.search(r'interface Port-channel(\S+)', l, re.M)
252        if match:
253            obj = {}
254            group = match.group(1)
255            obj['group'] = group
256            obj.update(get_channel(module, config, group))
257            objs.append(obj)
258
259    return objs
260
261
262def main():
263    """ main entry point for module execution
264    """
265    element_spec = dict(
266        group=dict(type='int'),
267        mode=dict(choices=['active', 'on', 'passive', 'auto', 'desirable']),
268        members=dict(type='list'),
269        state=dict(default='present',
270                   choices=['present', 'absent'])
271    )
272
273    aggregate_spec = deepcopy(element_spec)
274    aggregate_spec['group'] = dict(required=True)
275
276    required_one_of = [['group', 'aggregate']]
277    required_together = [['members', 'mode']]
278    mutually_exclusive = [['group', 'aggregate']]
279
280    # remove default in aggregate spec, to handle common arguments
281    remove_default_spec(aggregate_spec)
282
283    argument_spec = dict(
284        aggregate=dict(type='list', elements='dict', options=aggregate_spec,
285                       required_together=required_together),
286        purge=dict(default=False, type='bool')
287    )
288
289    argument_spec.update(element_spec)
290    argument_spec.update(ios_argument_spec)
291
292    module = AnsibleModule(argument_spec=argument_spec,
293                           required_one_of=required_one_of,
294                           required_together=required_together,
295                           mutually_exclusive=mutually_exclusive,
296                           supports_check_mode=True)
297
298    warnings = list()
299    result = {'changed': False}
300    if warnings:
301        result['warnings'] = warnings
302
303    want = map_params_to_obj(module)
304    have = map_config_to_obj(module)
305
306    commands = map_obj_to_commands((want, have), module)
307    result['commands'] = commands
308
309    if commands:
310        if not module.check_mode:
311            load_config(module, commands)
312        result['changed'] = True
313
314    module.exit_json(**result)
315
316
317if __name__ == '__main__':
318    main()
319