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