1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# (c) 2017, Ansible by Red Hat, inc
5#
6# This file is part of Ansible by Red Hat
7#
8# Ansible is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# Ansible is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
20#
21
22ANSIBLE_METADATA = {'metadata_version': '1.1',
23                    'status': ['preview'],
24                    'supported_by': 'network'}
25
26
27DOCUMENTATION = """
28---
29module: vyos_static_route
30version_added: "2.4"
31author: "Trishna Guha (@trishnaguha)"
32short_description: Manage static IP routes on Vyatta VyOS network devices
33description:
34  - This module provides declarative management of static
35    IP routes on Vyatta VyOS network devices.
36notes:
37  - Tested against VyOS 1.1.8 (helium).
38  - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html).
39options:
40  prefix:
41    description:
42      - Network prefix of the static route.
43        C(mask) param should be ignored if C(prefix) is provided
44        with C(mask) value C(prefix/mask).
45  mask:
46    description:
47      - Network prefix mask of the static route.
48  next_hop:
49    description:
50      - Next hop IP of the static route.
51  admin_distance:
52    description:
53      - Admin distance of the static route.
54  aggregate:
55    description: List of static route definitions
56  state:
57    description:
58      - State of the static route configuration.
59    default: present
60    choices: ['present', 'absent']
61extends_documentation_fragment: vyos
62"""
63
64EXAMPLES = """
65- name: configure static route
66  vyos_static_route:
67    prefix: 192.168.2.0
68    mask: 24
69    next_hop: 10.0.0.1
70
71- name: configure static route prefix/mask
72  vyos_static_route:
73    prefix: 192.168.2.0/16
74    next_hop: 10.0.0.1
75
76- name: remove configuration
77  vyos_static_route:
78    prefix: 192.168.2.0
79    mask: 16
80    next_hop: 10.0.0.1
81    state: absent
82
83- name: configure aggregates of static routes
84  vyos_static_route:
85    aggregate:
86      - { prefix: 192.168.2.0, mask: 24, next_hop: 10.0.0.1 }
87      - { prefix: 192.168.3.0, mask: 16, next_hop: 10.0.2.1 }
88      - { prefix: 192.168.3.0/16, next_hop: 10.0.2.1 }
89
90- name: Remove static route collections
91  vyos_static_route:
92    aggregate:
93      - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 }
94      - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 }
95    state: absent
96"""
97
98RETURN = """
99commands:
100  description: The list of configuration mode commands to send to the device
101  returned: always
102  type: list
103  sample:
104    - set protocols static route 192.168.2.0/16 next-hop 10.0.0.1
105"""
106import re
107
108from copy import deepcopy
109
110from ansible.module_utils.basic import AnsibleModule
111from ansible.module_utils.network.common.utils import remove_default_spec
112from ansible.module_utils.network.vyos.vyos import get_config, load_config
113from ansible.module_utils.network.vyos.vyos import vyos_argument_spec
114
115
116def spec_to_commands(updates, module):
117    commands = list()
118    want, have = updates
119    for w in want:
120        prefix = w['prefix']
121        mask = w['mask']
122        next_hop = w['next_hop']
123        admin_distance = w['admin_distance']
124        state = w['state']
125        del w['state']
126
127        if state == 'absent' and w in have:
128            commands.append('delete protocols static route %s/%s' % (prefix, mask))
129        elif state == 'present' and w not in have:
130            cmd = 'set protocols static route %s/%s next-hop %s' % (prefix, mask, next_hop)
131            if admin_distance != 'None':
132                cmd += ' distance %s' % (admin_distance)
133            commands.append(cmd)
134
135    return commands
136
137
138def config_to_dict(module):
139    data = get_config(module)
140    obj = []
141
142    for line in data.split('\n'):
143        if line.startswith('set protocols static route'):
144            match = re.search(r'static route (\S+)', line, re.M)
145            prefix = match.group(1).split('/')[0]
146            mask = match.group(1).split('/')[1]
147            if 'next-hop' in line:
148                match_hop = re.search(r'next-hop (\S+)', line, re.M)
149                next_hop = match_hop.group(1).strip("'")
150
151                match_distance = re.search(r'distance (\S+)', line, re.M)
152                if match_distance is not None:
153                    admin_distance = match_distance.group(1)[1:-1]
154                else:
155                    admin_distance = None
156
157                if admin_distance is not None:
158                    obj.append({'prefix': prefix,
159                                'mask': mask,
160                                'next_hop': next_hop,
161                                'admin_distance': admin_distance})
162                else:
163                    obj.append({'prefix': prefix,
164                                'mask': mask,
165                                'next_hop': next_hop,
166                                'admin_distance': 'None'})
167
168    return obj
169
170
171def map_params_to_obj(module, required_together=None):
172    obj = []
173    aggregate = module.params.get('aggregate')
174    if aggregate:
175        for item in aggregate:
176            for key in item:
177                if item.get(key) is None:
178                    item[key] = module.params[key]
179
180            module._check_required_together(required_together, item)
181            d = item.copy()
182            if '/' in d['prefix']:
183                d['mask'] = d['prefix'].split('/')[1]
184                d['prefix'] = d['prefix'].split('/')[0]
185
186            if 'admin_distance' in d:
187                d['admin_distance'] = str(d['admin_distance'])
188
189            obj.append(d)
190    else:
191        prefix = module.params['prefix'].strip()
192        if '/' in prefix:
193            mask = prefix.split('/')[1]
194            prefix = prefix.split('/')[0]
195        else:
196            mask = module.params['mask'].strip()
197        next_hop = module.params['next_hop'].strip()
198        admin_distance = str(module.params['admin_distance'])
199        state = module.params['state']
200
201        obj.append({
202            'prefix': prefix,
203            'mask': mask,
204            'next_hop': next_hop,
205            'admin_distance': admin_distance,
206            'state': state
207        })
208
209    return obj
210
211
212def main():
213    """ main entry point for module execution
214    """
215    element_spec = dict(
216        prefix=dict(type='str'),
217        mask=dict(type='str'),
218        next_hop=dict(type='str'),
219        admin_distance=dict(type='int'),
220        state=dict(default='present', choices=['present', 'absent'])
221    )
222
223    aggregate_spec = deepcopy(element_spec)
224    aggregate_spec['prefix'] = dict(required=True)
225
226    # remove default in aggregate spec, to handle common arguments
227    remove_default_spec(aggregate_spec)
228
229    argument_spec = dict(
230        aggregate=dict(type='list', elements='dict', options=aggregate_spec),
231    )
232
233    argument_spec.update(element_spec)
234    argument_spec.update(vyos_argument_spec)
235
236    required_one_of = [['aggregate', 'prefix']]
237    required_together = [['prefix', 'next_hop']]
238    mutually_exclusive = [['aggregate', 'prefix']]
239
240    module = AnsibleModule(argument_spec=argument_spec,
241                           required_one_of=required_one_of,
242                           required_together=required_together,
243                           mutually_exclusive=mutually_exclusive,
244                           supports_check_mode=True)
245
246    warnings = list()
247
248    result = {'changed': False}
249    if warnings:
250        result['warnings'] = warnings
251    want = map_params_to_obj(module, required_together=required_together)
252    have = config_to_dict(module)
253
254    commands = spec_to_commands((want, have), module)
255    result['commands'] = commands
256
257    if commands:
258        commit = not module.check_mode
259        load_config(module, commands, commit=commit)
260        result['changed'] = True
261
262    module.exit_json(**result)
263
264
265if __name__ == '__main__':
266    main()
267