1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright: (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_l3_interface 18version_added: "2.5" 19author: "Ganesh Nalawade (@ganeshrn)" 20short_description: Manage Layer-3 interfaces on Cisco IOS network devices. 21description: 22 - This module provides declarative management of Layer-3 interfaces 23 on IOS network devices. 24deprecated: 25 removed_in: '2.13' 26 alternative: ios_l3_interfaces 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 Layer-3 interface to be configured eg. GigabitEthernet0/2 34 ipv4: 35 description: 36 - IPv4 address to be set for the Layer-3 interface mentioned in I(name) option. 37 The address format is <ipv4 address>/<mask>, the mask is number 38 in range 0-32 eg. 192.168.0.1/24 39 ipv6: 40 description: 41 - IPv6 address to be set for the Layer-3 interface mentioned in I(name) option. 42 The address format is <ipv6 address>/<mask>, the mask is number 43 in range 0-128 eg. fd5d:12c9:2201:1::1/64 44 aggregate: 45 description: 46 - List of Layer-3 interfaces definitions. Each of the entry in aggregate list should 47 define name of interface C(name) and a optional C(ipv4) or C(ipv6) address. 48 state: 49 description: 50 - State of the Layer-3 interface configuration. It indicates if the configuration should 51 be present or absent on remote device. 52 default: present 53 choices: ['present', 'absent'] 54extends_documentation_fragment: ios 55""" 56 57EXAMPLES = """ 58- name: Remove GigabitEthernet0/3 IPv4 and IPv6 address 59 ios_l3_interface: 60 name: GigabitEthernet0/3 61 state: absent 62- name: Set GigabitEthernet0/3 IPv4 address 63 ios_l3_interface: 64 name: GigabitEthernet0/3 65 ipv4: 192.168.0.1/24 66- name: Set GigabitEthernet0/3 IPv6 address 67 ios_l3_interface: 68 name: GigabitEthernet0/3 69 ipv6: "fd5d:12c9:2201:1::1/64" 70- name: Set GigabitEthernet0/3 in dhcp 71 ios_l3_interface: 72 name: GigabitEthernet0/3 73 ipv4: dhcp 74 ipv6: dhcp 75- name: Set interface Vlan1 (SVI) IPv4 address 76 ios_l3_interface: 77 name: Vlan1 78 ipv4: 192.168.0.5/24 79- name: Set IP addresses on aggregate 80 ios_l3_interface: 81 aggregate: 82 - { name: GigabitEthernet0/3, ipv4: 192.168.2.10/24 } 83 - { name: GigabitEthernet0/3, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" } 84- name: Remove IP addresses on aggregate 85 ios_l3_interface: 86 aggregate: 87 - { name: GigabitEthernet0/3, ipv4: 192.168.2.10/24 } 88 - { name: GigabitEthernet0/3, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" } 89 state: absent 90""" 91 92RETURN = """ 93commands: 94 description: The list of configuration mode commands to send to the device 95 returned: always, except for the platforms that use Netconf transport to manage the device. 96 type: list 97 sample: 98 - interface GigabitEthernet0/2 99 - ip address 192.168.0.1 255.255.255.0 100 - ipv6 address fd5d:12c9:2201:1::1/64 101""" 102import re 103 104from copy import deepcopy 105 106from ansible.module_utils._text import to_text 107from ansible.module_utils.basic import AnsibleModule 108from ansible.module_utils.network.ios.ios import get_config, load_config 109from ansible.module_utils.network.ios.ios import ios_argument_spec 110from ansible.module_utils.network.common.config import NetworkConfig 111from ansible.module_utils.network.common.utils import remove_default_spec 112from ansible.module_utils.network.common.utils import is_netmask, is_masklen, to_netmask, to_masklen 113 114 115def validate_ipv4(value, module): 116 if value: 117 address = value.split('/') 118 if len(address) != 2: 119 module.fail_json(msg='address format is <ipv4 address>/<mask>, got invalid format %s' % value) 120 121 if not is_masklen(address[1]): 122 module.fail_json(msg='invalid value for mask: %s, mask should be in range 0-32' % address[1]) 123 124 125def validate_ipv6(value, module): 126 if value: 127 address = value.split('/') 128 if len(address) != 2: 129 module.fail_json(msg='address format is <ipv6 address>/<mask>, got invalid format %s' % value) 130 else: 131 if not 0 <= int(address[1]) <= 128: 132 module.fail_json(msg='invalid value for mask: %s, mask should be in range 0-128' % address[1]) 133 134 135def validate_param_values(module, obj, param=None): 136 if param is None: 137 param = module.params 138 for key in obj: 139 # validate the param value (if validator func exists) 140 validator = globals().get('validate_%s' % key) 141 if callable(validator): 142 validator(param.get(key), module) 143 144 145def parse_config_argument(configobj, name, arg=None): 146 cfg = configobj['interface %s' % name] 147 cfg = '\n'.join(cfg.children) 148 149 values = [] 150 matches = re.finditer(r'%s (.+)$' % arg, cfg, re.M) 151 for match in matches: 152 match_str = match.group(1).strip() 153 if arg == 'ipv6 address': 154 values.append(match_str) 155 else: 156 values = match_str 157 break 158 159 return values or None 160 161 162def search_obj_in_list(name, lst): 163 for o in lst: 164 if o['name'] == name: 165 return o 166 167 return None 168 169 170def map_obj_to_commands(updates, module): 171 commands = list() 172 want, have = updates 173 for w in want: 174 name = w['name'] 175 ipv4 = w['ipv4'] 176 ipv6 = w['ipv6'] 177 state = w['state'] 178 179 interface = 'interface ' + name 180 commands.append(interface) 181 182 obj_in_have = search_obj_in_list(name, have) 183 if state == 'absent' and obj_in_have: 184 if obj_in_have['ipv4']: 185 if ipv4: 186 address = ipv4.split('/') 187 if len(address) == 2: 188 ipv4 = '{0} {1}'.format(address[0], to_netmask(address[1])) 189 commands.append('no ip address {0}'.format(ipv4)) 190 else: 191 commands.append('no ip address') 192 if obj_in_have['ipv6']: 193 if ipv6: 194 commands.append('no ipv6 address {0}'.format(ipv6)) 195 else: 196 commands.append('no ipv6 address') 197 if 'dhcp' in obj_in_have['ipv6']: 198 commands.append('no ipv6 address dhcp') 199 200 elif state == 'present': 201 if ipv4: 202 if obj_in_have is None or obj_in_have.get('ipv4') is None or ipv4 != obj_in_have['ipv4']: 203 address = ipv4.split('/') 204 if len(address) == 2: 205 ipv4 = '{0} {1}'.format(address[0], to_netmask(address[1])) 206 commands.append('ip address {0}'.format(ipv4)) 207 208 if ipv6: 209 if obj_in_have is None or obj_in_have.get('ipv6') is None or ipv6.lower() not in [addr.lower() for addr in obj_in_have['ipv6']]: 210 commands.append('ipv6 address {0}'.format(ipv6)) 211 212 if commands[-1] == interface: 213 commands.pop(-1) 214 215 return commands 216 217 218def map_config_to_obj(module): 219 config = get_config(module) 220 configobj = NetworkConfig(indent=1, contents=config) 221 222 match = re.findall(r'^interface (\S+)', config, re.M) 223 if not match: 224 return list() 225 226 instances = list() 227 228 for item in set(match): 229 ipv4 = parse_config_argument(configobj, item, 'ip address') 230 if ipv4: 231 # eg. 192.168.2.10 255.255.255.0 -> 192.168.2.10/24 232 address = ipv4.strip().split(' ') 233 if len(address) == 2 and is_netmask(address[1]): 234 ipv4 = '{0}/{1}'.format(address[0], to_text(to_masklen(address[1]))) 235 236 obj = { 237 'name': item, 238 'ipv4': ipv4, 239 'ipv6': parse_config_argument(configobj, item, 'ipv6 address'), 240 'state': 'present' 241 } 242 instances.append(obj) 243 244 return instances 245 246 247def map_params_to_obj(module): 248 obj = [] 249 250 aggregate = module.params.get('aggregate') 251 if aggregate: 252 for item in aggregate: 253 for key in item: 254 if item.get(key) is None: 255 item[key] = module.params[key] 256 257 validate_param_values(module, item, item) 258 obj.append(item.copy()) 259 else: 260 obj.append({ 261 'name': module.params['name'], 262 'ipv4': module.params['ipv4'], 263 'ipv6': module.params['ipv6'], 264 'state': module.params['state'] 265 }) 266 267 validate_param_values(module, obj) 268 269 return obj 270 271 272def main(): 273 """ main entry point for module execution 274 """ 275 element_spec = dict( 276 name=dict(), 277 ipv4=dict(), 278 ipv6=dict(), 279 state=dict(default='present', 280 choices=['present', 'absent']) 281 ) 282 283 aggregate_spec = deepcopy(element_spec) 284 aggregate_spec['name'] = dict(required=True) 285 286 # remove default in aggregate spec, to handle common arguments 287 remove_default_spec(aggregate_spec) 288 289 argument_spec = dict( 290 aggregate=dict(type='list', elements='dict', options=aggregate_spec), 291 ) 292 293 argument_spec.update(element_spec) 294 argument_spec.update(ios_argument_spec) 295 296 required_one_of = [['name', 'aggregate']] 297 mutually_exclusive = [['name', 'aggregate']] 298 module = AnsibleModule(argument_spec=argument_spec, 299 required_one_of=required_one_of, 300 mutually_exclusive=mutually_exclusive, 301 supports_check_mode=True) 302 303 warnings = list() 304 305 result = {'changed': False} 306 307 want = map_params_to_obj(module) 308 have = map_config_to_obj(module) 309 310 commands = map_obj_to_commands((want, have), module) 311 result['commands'] = commands 312 313 if commands: 314 if not module.check_mode: 315 resp = load_config(module, commands) 316 warnings.extend((out for out in resp if out)) 317 318 result['changed'] = True 319 320 if warnings: 321 result['warnings'] = warnings 322 323 module.exit_json(**result) 324 325 326if __name__ == '__main__': 327 main() 328