|
| 1 | +#!/usr/bin/python3 |
| 2 | +""" |
| 3 | +Tool converting Junos "set" output to our YAML configuration format for use with Salt |
| 4 | +Copyright (C) 2023 SUSE LLC <[email protected]> |
| 5 | +
|
| 6 | +This program is free software: you can redistribute it and/or modify |
| 7 | +it under the terms of the GNU General Public License as published by |
| 8 | +the Free Software Foundation, either version 3 of the License, or |
| 9 | +(at your option) any later version. |
| 10 | +
|
| 11 | +This program is distributed in the hope that it will be useful, |
| 12 | +but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +GNU General Public License for more details. |
| 15 | +
|
| 16 | +You should have received a copy of the GNU General Public License |
| 17 | +along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 18 | +""" |
| 19 | +import re |
| 20 | + |
| 21 | +import json |
| 22 | +import yaml |
| 23 | + |
| 24 | +infile = 'input.txt' |
| 25 | +outfile = 'output.yaml' |
| 26 | + |
| 27 | +confmap = {'applications': {}, 'interfaces': {}, 'subinterfaces': {}, 'zones': {}, 'addresses': {}, 'address_sets': {}, 'policies': [], 'snats': {}, 'proxyarps': {}, 'snat_rules': {}, 'static_nat_rules': {}, 'sroutes': [], 'sroutes6': []} |
| 28 | +outmap = {'services': [], 'interfaces': {}, 'zones': [], 'addresses': [], 'address_groups': [], 'policies': [], 'source_nat_pools': [], 'proxy_arp': [], 'source_nat_rules': [], 'static_nat_rules': [], 'sroutes': [], 'sroutes6': [], 'vlans': {}} |
| 29 | +discard = ['mtu'] |
| 30 | +lacp_key = 'lacp' |
| 31 | +mclag_key = 'mc' |
| 32 | + |
| 33 | +def do_description(description): |
| 34 | + return 'description', ' '.join(description).replace('"','') |
| 35 | + |
| 36 | +with open(infile, 'r') as fh: |
| 37 | + for line in fh: |
| 38 | + ls = line.replace('\n', '').split(' ') |
| 39 | + lsl = len(ls) |
| 40 | + if lsl < 5: |
| 41 | + continue |
| 42 | + match ls[1]: |
| 43 | + case 'applications': |
| 44 | + name = ls[3] |
| 45 | + if not name in confmap['applications']: |
| 46 | + confmap['applications'].update({name: {}}) |
| 47 | + confmap['applications'][name].update({ls[4]: ls[5]}) |
| 48 | + case 'interfaces': |
| 49 | + name = ls[2] |
| 50 | + if not name in confmap['interfaces']: |
| 51 | + confmap['interfaces'].update({name: {}}) |
| 52 | + mymap = confmap['interfaces'][name] |
| 53 | + if ls[3] == 'unit': |
| 54 | + unit = int(ls[4]) |
| 55 | + #if not 'units' in mymap: |
| 56 | + # mymap.update({'units': {}}) |
| 57 | + #myunits = mymap['units'] |
| 58 | + #if not unit in myunits: |
| 59 | + # myunits.update({unit: {}}) |
| 60 | + # myunit = myunits[unit] |
| 61 | + if unit == 0: |
| 62 | + match ls[5]: |
| 63 | + case 'family': |
| 64 | + if ls[6] == 'ethernet-switching' and ls[7] in ['vlan', 'interface-mode']: |
| 65 | + #if not 'vlan' in myunit: |
| 66 | + # myunit.update({'vlan': {}}) |
| 67 | + #myvlan = myunit['vlan'] |
| 68 | + if not 'vlan' in mymap: |
| 69 | + mymap.update({'vlan': {}}) |
| 70 | + myvlan = mymap['vlan'] |
| 71 | + if ls[8] == 'members': |
| 72 | + if not 'ids' in myvlan: |
| 73 | + myvlan['ids'] = [] |
| 74 | + myvlan['ids'].append(ls[9]) |
| 75 | + elif ls[7] == 'interface-mode': |
| 76 | + myvlan['type'] = ls[8] |
| 77 | + # currently neither needed nor supported by the configuration |
| 78 | + #elif unit != 0: |
| 79 | + # name = f'{name}.{ls[4]}' |
| 80 | + # if not name in confmap['subinterfaces']: |
| 81 | + # confmap['subinterfaces'].update({name: {}}) |
| 82 | + # mymap = confmap['subinterfaces'][name] |
| 83 | + mykey, myval = None, None |
| 84 | + if ls[3] == 'description': |
| 85 | + mykey, myval = do_description(ls[4:]) |
| 86 | + elif ls[3] == 'mtu': |
| 87 | + mymap['mtu'] = int(ls[4]) |
| 88 | + elif lsl >= 7 and ls[5] == 'description': |
| 89 | + mykey, myval = do_description(ls[6:]) |
| 90 | + elif lsl == 6 or (lsl == 7 and ls[4] == 'lacp') or (lsl == 7 and ls[4] == 'mc-ae'): |
| 91 | + mykey, myval = ls[4], ls[5] |
| 92 | + match mykey: |
| 93 | + case 'redundancy-group': |
| 94 | + # set interfaces reth0 redundant-ether-options redundancy-group 1 |
| 95 | + mykey = lacp_key |
| 96 | + myval = {'group': int(myval)} |
| 97 | + case 'lacp': |
| 98 | + mykey = lacp_key |
| 99 | + if lsl == 6: |
| 100 | + # set interfaces reth0 redundant-ether-options lacp active |
| 101 | + myval = {'mode': myval} |
| 102 | + elif lsl == 7: |
| 103 | + # set interfaces reth0 redundant-ether-options lacp periodic fast |
| 104 | + myval = {ls[5]: ls[6]} |
| 105 | + case 'mc-ae': |
| 106 | + mykey = mclag_key |
| 107 | + myval = {ls[5]: ls[6]} |
| 108 | + case '802.3ad': |
| 109 | + mykey = lacp_key |
| 110 | + myval = ls[5] |
| 111 | + elif lsl == 9: |
| 112 | + if ls[3] == 'unit': |
| 113 | + myval = ls[8] |
| 114 | + match ls[6]: |
| 115 | + case 'inet': |
| 116 | + # set interfaces reth0 unit 0 family inet address 195.135.223.5/28 |
| 117 | + #mykey = 'addr' |
| 118 | + mykey = 'addresses' |
| 119 | + case 'inet6': |
| 120 | + # set interfaces reth0 unit 0 family inet6 address 2a07:de40:b260:0001::5/64 |
| 121 | + #mykey = 'addr6' |
| 122 | + mykey = 'addresses' |
| 123 | + elif lsl == 5: |
| 124 | + mykey, myval = ls[3], ls[4] |
| 125 | + #else: |
| 126 | + #print(f'Dropped line: {ls}') |
| 127 | + if mykey and myval: |
| 128 | + #print(mykey) |
| 129 | + if mykey in ['lacp', 'mc'] and lsl > 6: |
| 130 | + if not 'ae' in mymap: |
| 131 | + mymap['ae'] = {} |
| 132 | + myae = mymap['ae'] |
| 133 | + lowmap = myae |
| 134 | + else: |
| 135 | + lowmap = mymap |
| 136 | + if mykey in lowmap: |
| 137 | + if isinstance(myval, dict): |
| 138 | + # add to existing lacp_options |
| 139 | + lowmap[mykey].update(myval) |
| 140 | + elif isinstance(mykey[mykey], list): |
| 141 | + lowmap[mykey].append(myval) |
| 142 | + elif mykey not in discard: |
| 143 | + if mykey.startswith('addr'): |
| 144 | + myval = [myval] |
| 145 | + lowmap.update({mykey: myval}) |
| 146 | + case 'security': |
| 147 | + if ls[3] == 'security-zone': |
| 148 | + name = ls[4] |
| 149 | + if not name in confmap['zones']: |
| 150 | + confmap['zones'].update({name: {'interfaces': [], 'local-services': []}}) |
| 151 | + match ls[5]: |
| 152 | + case 'interfaces': |
| 153 | + ifname = ls[6] |
| 154 | + iflist = confmap['zones'][name]['interfaces'] |
| 155 | + if not ifname in iflist: |
| 156 | + iflist.append(ifname) |
| 157 | + case 'host-inbound-traffic': |
| 158 | + svname = ls[7] |
| 159 | + svlist = confmap['zones'][name]['local-services'] |
| 160 | + if not svname in svlist: |
| 161 | + svlist.append(svname) |
| 162 | + elif ls[2] == 'address-book': |
| 163 | + name = ls[5] |
| 164 | + match ls[4]: |
| 165 | + case 'address': |
| 166 | + addressmap = confmap['addresses'] |
| 167 | + address = ' '.join(ls[6:]) |
| 168 | + addressmap.update({name: address}) |
| 169 | + case 'address-set': |
| 170 | + addressmap = confmap['address_sets'] |
| 171 | + if not name in addressmap: |
| 172 | + addressmap.update({name: []}) |
| 173 | + addressmap[name].append(ls[7]) |
| 174 | + elif ls[2] == 'policies': |
| 175 | + #if ls[7] == 'policy': |
| 176 | + # policy = ls[8] |
| 177 | + #if policy not in confmap['policies']: |
| 178 | + # confmap['policies'].update({policy: {'match': {'sources': [], 'destinations': [], 'applications': []}}}) |
| 179 | + #policymap = confmap['policies'][policy] |
| 180 | + policymap = confmap['policies'] |
| 181 | + |
| 182 | + mymap = {'match': {'sources': [], 'destinations': [], 'applications': []}} |
| 183 | + |
| 184 | + if ls[7] == 'policy': |
| 185 | + policy = ls[8] |
| 186 | + mymap.update({'name': policy}) |
| 187 | + if ls[3] == 'from-zone': |
| 188 | + from_zone = ls[4] |
| 189 | + mymap.update({'from_zone': from_zone}) |
| 190 | + if ls[5] == 'to-zone': |
| 191 | + to_zone = ls[6] |
| 192 | + mymap.update({'to_zone': to_zone}) |
| 193 | + if ls[9] == 'match': |
| 194 | + match ls[10]: |
| 195 | + case 'source-address': |
| 196 | + source = ls[11] |
| 197 | + mymap['match']['sources'].append(source) |
| 198 | + case 'destination-address': |
| 199 | + destination = ls[11] |
| 200 | + mymap['match']['destinations'].append(destination) |
| 201 | + case 'application': |
| 202 | + application = ls[11] |
| 203 | + mymap['match']['applications'].append(application) |
| 204 | + elif ls[9] == 'then': |
| 205 | + mymap.update({'action': ls[10]}) |
| 206 | + |
| 207 | + policymap.append(mymap) |
| 208 | + |
| 209 | + elif ls[2] == 'nat': |
| 210 | + if ls[3] in ['source', 'static']: |
| 211 | + name = ls[5] |
| 212 | + if ls[4] == 'pool': |
| 213 | + if ls[6] == 'address': |
| 214 | + address = ls[7] |
| 215 | + confmap['snats'].update({name: address}) |
| 216 | + elif ls[4] == 'rule-set': |
| 217 | + if ls[3] == 'source': |
| 218 | + confname = 'snat_rules' |
| 219 | + elif ls[3] == 'static': |
| 220 | + confname = 'static_nat_rules' |
| 221 | + rulesetmap = confmap[confname] |
| 222 | + if not name in rulesetmap: |
| 223 | + rulesetmap.update({name: {'rules': {}}}) |
| 224 | + if ls[6] in ['from', 'to'] and ls[7] == 'zone': |
| 225 | + rulesetmap[name].update({ls[6]: ls[8]}) |
| 226 | + if ls[6] == 'rule': |
| 227 | + rulename = ls[7] |
| 228 | + if not rulename in rulesetmap[name]['rules']: |
| 229 | + rulesetmap[name]['rules'].update({rulename: {'sources': [], 'destinations': []}}) |
| 230 | + rulemap = rulesetmap[name]['rules'][rulename] |
| 231 | + if ls[8] == 'match' and ls[9] == 'source-address': |
| 232 | + rulemap['sources'].append(ls[10]) |
| 233 | + if ls[8] == 'match' and ls[9] == 'destination-address': |
| 234 | + # we don't seem to use this and assume everything? |
| 235 | + rulemap['destinations'].append(ls[10]) |
| 236 | + elif ls[8] == 'then': |
| 237 | + if ls[9] == 'source-nat': |
| 238 | + if ls[10] == 'pool': |
| 239 | + rulemap.update({'pool': ls[11]}) |
| 240 | + elif ls[9] == 'static-nat': |
| 241 | + if ls[10] == 'prefix': |
| 242 | + rulemap.update({'prefix': ls[11]}) |
| 243 | + if not rulemap['destinations']: |
| 244 | + # feels wrong, it's in the expected output file but not in the input file |
| 245 | + rulemap['destinations'].append('0.0.0.0/0') |
| 246 | + |
| 247 | + elif ls[3] == 'proxy-arp': |
| 248 | + if ls[4] == 'interface' and ls[6] == 'address': |
| 249 | + interface = ls[5] |
| 250 | + address = ls[7] |
| 251 | + confmap['proxyarps'].update({interface: address}) |
| 252 | + |
| 253 | + case 'routing-options': |
| 254 | + if ls[2] == 'static' and ls[3] == 'route' and ls[5] == 'next-hop': |
| 255 | + route = ls[4] |
| 256 | + hop = ls[6] |
| 257 | + confmap['sroutes'].append({'dst': route, 'gw': hop}) |
| 258 | + elif ls[2] == 'rib' and ls[3] == 'inet6.0' and ls[4] == 'static' and ls[5] == 'route' and ls[7] == 'next-hop': |
| 259 | + route = ls[6] |
| 260 | + hop = ls[8] |
| 261 | + confmap['sroutes6'].append({'dst': route, 'gw': hop}) |
| 262 | + |
| 263 | + case 'vlans': |
| 264 | + vlan = ls[2] |
| 265 | + match ls[3]: |
| 266 | + case 'vlan-id': |
| 267 | + mykey = 'id' |
| 268 | + myval = int(ls[4]) |
| 269 | + case 'description': |
| 270 | + mykey, myval = do_description(ls[4:]) |
| 271 | + mymap = outmap['vlans'] |
| 272 | + if vlan in mymap: |
| 273 | + mymap[vlan].update({mykey: myval}) |
| 274 | + else: |
| 275 | + mymap.update({vlan: {mykey: myval}}) |
| 276 | + |
| 277 | +scrubbed_policies = [] |
| 278 | +for policy in confmap['policies']: |
| 279 | + scrubbed = policy.copy() |
| 280 | + scrubbed.pop('match') |
| 281 | + if 'action' in scrubbed: |
| 282 | + scrubbed.pop('action') |
| 283 | + scrubbed_policies.append(scrubbed) |
| 284 | + |
| 285 | +unique_policies = [dict(t) for t in {tuple(d.items()) for d in scrubbed_policies}] |
| 286 | + |
| 287 | +for policy in unique_policies: |
| 288 | + policymap = {'fromzn': policy['from_zone'], 'tozn': policy['to_zone'], 'name': policy['name'], 'srcs': [], 'dsts': [], 'applications': []} |
| 289 | + for origpolicy in confmap['policies']: |
| 290 | + if origpolicy['name'] == policy['name'] and origpolicy['from_zone'] == policy['from_zone'] and origpolicy['to_zone'] == policy['to_zone']: |
| 291 | + new_sources = set(origpolicy['match']['sources']) - set(policymap['srcs']) |
| 292 | + if new_sources: |
| 293 | + policymap['srcs'].extend(list(new_sources)) |
| 294 | + new_destinations = set(origpolicy['match']['destinations']) - set(policymap['dsts']) |
| 295 | + if new_destinations: |
| 296 | + policymap['dsts'].extend(list(new_destinations)) |
| 297 | + new_applications = set(origpolicy['match']['applications']) - set(policymap['applications']) |
| 298 | + if new_applications: |
| 299 | + policymap['applications'].extend(list(new_applications)) |
| 300 | + if 'action' in origpolicy: |
| 301 | + policymap.update({'action': origpolicy['action']}) |
| 302 | + if not policymap['srcs']: |
| 303 | + del policymap['srcs'] |
| 304 | + if not policymap['dsts']: |
| 305 | + del policymap['dsts'] |
| 306 | + if not policymap['applications']: |
| 307 | + del policymap['applications'] |
| 308 | + outmap['policies'].append(policymap) |
| 309 | + |
| 310 | +for config in confmap.keys(): |
| 311 | + if isinstance(confmap[config], list): |
| 312 | + if not config == 'policies': |
| 313 | + # currently used for sroutes and sroutes6 |
| 314 | + outmap[config].extend(confmap[config]) |
| 315 | + continue |
| 316 | + for name, lowconfig in confmap[config].items(): |
| 317 | + match config: |
| 318 | + case 'applications': |
| 319 | + outport = lowconfig.get('destination-port', 'N/A') |
| 320 | + try: |
| 321 | + outport = int(outport) |
| 322 | + except ValueError: |
| 323 | + outport = outport |
| 324 | + outconfig = {'name': name, 'proto': lowconfig['protocol'], 'port': outport} |
| 325 | + outmap['services'].append(outconfig) |
| 326 | + case 'interfaces': |
| 327 | + outmap['interfaces'].update({name: lowconfig}) |
| 328 | + #case 'subinterfaces': |
| 329 | + # mymap = {'ifname': name} |
| 330 | + # mymap.update(lowconfig) |
| 331 | + # mymap.update({'vlanid': int(re.search(r'\d+$', name).group(0))}) |
| 332 | + # outmap['subinterfaces'].append(mymap) |
| 333 | + case 'zones': |
| 334 | + mymap = {'name': name} |
| 335 | + mymap.update(lowconfig) |
| 336 | + outmap['zones'].append(mymap) |
| 337 | + case 'addresses': |
| 338 | + mymap = {'name': name, 'prefix': lowconfig} |
| 339 | + outmap['addresses'].append(mymap) |
| 340 | + case 'address_sets': |
| 341 | + mymap = {'name': name, 'addresses': lowconfig} |
| 342 | + outmap['address_groups'].append(mymap) |
| 343 | + # policies are now treated in a special clinic |
| 344 | + #case 'policies': |
| 345 | + # match = lowconfig['match'] |
| 346 | + # mymap = {'fromzn': lowconfig['from_zone'], 'tozn': lowconfig['to_zone'], 'name': name, |
| 347 | + # 'srcs': match['sources'], 'dsts': match['destinations'], 'applications': match['applications'], |
| 348 | + # 'action': lowconfig['action']} |
| 349 | + # outmap['policies'].append(mymap) |
| 350 | + case 'snats': |
| 351 | + mymap = {'name': name, 'prefix': lowconfig} |
| 352 | + outmap['source_nat_pools'].append(mymap) |
| 353 | + case 'proxyarps': |
| 354 | + mymap = {'interface': name, 'prefix': lowconfig} |
| 355 | + outmap['proxy_arp'].append(mymap) |
| 356 | + case 'snat_rules': |
| 357 | + mymap = {'set': name, 'fromzn': lowconfig['from'], 'tozn': lowconfig['to'], 'rules': []} |
| 358 | + for rule, ruleconfig in lowconfig['rules'].items(): |
| 359 | + mymap['rules'].append({'name': rule, 'srcs': ruleconfig['sources'], 'dsts': ruleconfig['destinations'], 'pool': ruleconfig['pool']}) |
| 360 | + outmap['source_nat_rules'].append(mymap) |
| 361 | + case 'static_nat_rules': |
| 362 | + mymap = {'set': name, 'fromzn': lowconfig['from'], 'rules': []} |
| 363 | + for rule, ruleconfig in lowconfig['rules'].items(): |
| 364 | + thismap = {'name': rule, 'dsts': ruleconfig['destinations'], 'natprefix': ruleconfig['prefix']} |
| 365 | + if ruleconfig['sources']: |
| 366 | + # we don't seem to use this |
| 367 | + thismap.update({'srcs': ruleconfig['sources']}) |
| 368 | + mymap['rules'].append(thismap) |
| 369 | + outmap['static_nat_rules'].append(mymap) |
| 370 | + |
| 371 | +with open('debug', 'w') as fh: |
| 372 | + json.dump(confmap, fh) |
| 373 | + |
| 374 | +dumpmap = {} |
| 375 | +dumpmap['devices'] = {'FIXME': {}} |
| 376 | +dd = dumpmap['devices']['FIXME'] |
| 377 | + |
| 378 | +for key, value in outmap.items(): |
| 379 | + if key: |
| 380 | + pair = {key: value} |
| 381 | + # to-do: maybe differentiate between switching and firewall keys? |
| 382 | + if key in ['vlans']: |
| 383 | + dumpmap.update(pair) |
| 384 | + elif key in ['interfaces']: |
| 385 | + dd.update(pair) |
| 386 | + |
| 387 | +if not dd: |
| 388 | + dumpmap.clear() |
| 389 | + |
| 390 | +with open(outfile, 'w') as fh: |
| 391 | +# yaml.Dumper.ignore_aliases = lambda *args : True |
| 392 | + yaml.dump(dumpmap, fh, sort_keys=False) |
| 393 | + |
| 394 | +# yes, I discovered Junos JSON output too late. |
0 commit comments