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