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