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