github.com/osrg/gobgp@v2.0.0+incompatible/test/lib/quagga.py (about) 1 # Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 # implied. 13 # See the License for the specific language governing permissions and 14 # limitations under the License. 15 16 from __future__ import absolute_import 17 18 import re 19 20 from fabric import colors 21 from fabric.utils import indent 22 import netaddr 23 24 from lib.base import ( 25 wait_for_completion, 26 BGPContainer, 27 OSPFContainer, 28 CmdBuffer, 29 BGP_FSM_IDLE, 30 BGP_FSM_ACTIVE, 31 BGP_FSM_ESTABLISHED, 32 BGP_ATTR_TYPE_MULTI_EXIT_DISC, 33 BGP_ATTR_TYPE_LOCAL_PREF, 34 ) 35 36 37 class QuaggaBGPContainer(BGPContainer): 38 39 WAIT_FOR_BOOT = 1 40 SHARED_VOLUME = '/etc/quagga' 41 42 def __init__(self, name, asn, router_id, ctn_image_name='osrg/quagga', bgpd_config=None, zebra=False): 43 super(QuaggaBGPContainer, self).__init__(name, asn, router_id, 44 ctn_image_name) 45 self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) 46 self.zebra = zebra 47 48 # bgp_config is equivalent to config.BgpConfigSet structure 49 # Example: 50 # bgpd_config = { 51 # 'global': { 52 # 'confederation': { 53 # 'identifier': 10, 54 # 'peers': [65001], 55 # }, 56 # }, 57 # } 58 self.bgpd_config = bgpd_config or {} 59 60 def _get_enabled_daemons(self): 61 daemons = ['bgpd'] 62 if self.zebra: 63 daemons.append('zebra') 64 return daemons 65 66 def _is_running(self): 67 def f(d): 68 return self.local( 69 'vtysh -d {0} -c "show version"' 70 ' > /dev/null 2>&1; echo $?'.format(d), capture=True) == '0' 71 72 return all([f(d) for d in self._get_enabled_daemons()]) 73 74 def _wait_for_boot(self): 75 wait_for_completion(self._is_running) 76 77 def run(self): 78 super(QuaggaBGPContainer, self).run() 79 self._wait_for_boot() 80 return self.WAIT_FOR_BOOT 81 82 def get_global_rib(self, prefix='', rf='ipv4'): 83 rib = [] 84 if prefix != '': 85 return self.get_global_rib_with_prefix(prefix, rf) 86 87 out = self.vtysh('show bgp {0} unicast'.format(rf), config=False) 88 if out.startswith('No BGP network exists'): 89 return rib 90 91 for line in out.split('\n')[6:-2]: 92 line = line[3:] 93 94 p = line.split()[0] 95 if '/' not in p: 96 continue 97 98 rib.extend(self.get_global_rib_with_prefix(p, rf)) 99 100 return rib 101 102 def get_global_rib_with_prefix(self, prefix, rf): 103 rib = [] 104 105 lines = [line.strip() for line in self.vtysh('show bgp {0} unicast {1}'.format(rf, prefix), config=False).split('\n')] 106 107 if lines[0] == '% Network not in table': 108 return rib 109 110 lines = lines[2:] 111 112 if lines[0].startswith('Not advertised'): 113 lines.pop(0) # another useless line 114 elif lines[0].startswith('Advertised to non peer-group peers:'): 115 lines = lines[2:] # other useless lines 116 else: 117 raise Exception('unknown output format {0}'.format(lines)) 118 119 while len(lines) > 0: 120 if lines[0] == 'Local': 121 aspath = [] 122 else: 123 aspath = [int(re.sub('\D', '', asn)) for asn in lines[0].split()] 124 125 nexthop = lines[1].split()[0].strip() 126 info = [s.strip(',') for s in lines[2].split()] 127 attrs = [] 128 ibgp = False 129 best = False 130 if 'metric' in info: 131 med = info[info.index('metric') + 1] 132 attrs.append({'type': BGP_ATTR_TYPE_MULTI_EXIT_DISC, 'metric': int(med)}) 133 if 'localpref' in info: 134 localpref = info[info.index('localpref') + 1] 135 attrs.append({'type': BGP_ATTR_TYPE_LOCAL_PREF, 'value': int(localpref)}) 136 if 'internal' in info: 137 ibgp = True 138 if 'best' in info: 139 best = True 140 141 rib.append({'prefix': prefix, 'nexthop': nexthop, 142 'aspath': aspath, 'attrs': attrs, 'ibgp': ibgp, 'best': best}) 143 144 lines = lines[5:] 145 146 return rib 147 148 def get_neighbor_state(self, peer): 149 if peer not in self.peers: 150 raise Exception('not found peer {0}'.format(peer.router_id)) 151 152 neigh_addr = self.peers[peer]['neigh_addr'].split('/')[0] 153 154 info = [l.strip() for l in self.vtysh('show bgp neighbors {0}'.format(neigh_addr), config=False).split('\n')] 155 156 if not info[0].startswith('BGP neighbor is'): 157 raise Exception('unknown format') 158 159 idx1 = info[0].index('BGP neighbor is ') 160 idx2 = info[0].index(',') 161 n_addr = info[0][idx1 + len('BGP neighbor is '):idx2] 162 if n_addr == neigh_addr: 163 idx1 = info[2].index('= ') 164 state = info[2][idx1 + len('= '):] 165 if state.startswith('Idle'): 166 return BGP_FSM_IDLE 167 elif state.startswith('Active'): 168 return BGP_FSM_ACTIVE 169 elif state.startswith('Established'): 170 return BGP_FSM_ESTABLISHED 171 else: 172 return state 173 174 raise Exception('not found peer {0}'.format(peer.router_id)) 175 176 def send_route_refresh(self): 177 self.vtysh('clear ip bgp * soft', config=False) 178 179 def create_config(self): 180 self._create_config_bgp() 181 if self.zebra: 182 self._create_config_zebra() 183 184 def _create_config_bgp(self): 185 186 c = CmdBuffer() 187 c << 'hostname bgpd' 188 c << 'password zebra' 189 c << 'router bgp {0}'.format(self.asn) 190 c << 'bgp router-id {0}'.format(self.router_id) 191 if any(info['graceful_restart'] for info in self.peers.itervalues()): 192 c << 'bgp graceful-restart' 193 194 if 'global' in self.bgpd_config: 195 if 'confederation' in self.bgpd_config['global']: 196 conf = self.bgpd_config['global']['confederation']['config'] 197 c << 'bgp confederation identifier {0}'.format(conf['identifier']) 198 c << 'bgp confederation peers {0}'.format(' '.join([str(i) for i in conf['member-as-list']])) 199 200 version = 4 201 for peer, info in self.peers.iteritems(): 202 version = netaddr.IPNetwork(info['neigh_addr']).version 203 n_addr = info['neigh_addr'].split('/')[0] 204 if version == 6: 205 c << 'no bgp default ipv4-unicast' 206 c << 'neighbor {0} remote-as {1}'.format(n_addr, info['remote_as']) 207 # For rapid convergence 208 c << 'neighbor {0} advertisement-interval 1'.format(n_addr) 209 if info['is_rs_client']: 210 c << 'neighbor {0} route-server-client'.format(n_addr) 211 for typ, p in info['policies'].iteritems(): 212 c << 'neighbor {0} route-map {1} {2}'.format(n_addr, p['name'], 213 typ) 214 if info['passwd']: 215 c << 'neighbor {0} password {1}'.format(n_addr, info['passwd']) 216 if info['passive']: 217 c << 'neighbor {0} passive'.format(n_addr) 218 if version == 6: 219 c << 'address-family ipv6 unicast' 220 c << 'neighbor {0} activate'.format(n_addr) 221 c << 'exit-address-family' 222 223 if self.zebra: 224 if version == 6: 225 c << 'address-family ipv6 unicast' 226 c << 'redistribute connected' 227 c << 'exit-address-family' 228 else: 229 c << 'redistribute connected' 230 231 for name, policy in self.policies.iteritems(): 232 c << 'access-list {0} {1} {2}'.format(name, policy['type'], 233 policy['match']) 234 c << 'route-map {0} permit 10'.format(name) 235 c << 'match ip address {0}'.format(name) 236 c << 'set metric {0}'.format(policy['med']) 237 238 c << 'debug bgp as4' 239 c << 'debug bgp fsm' 240 c << 'debug bgp updates' 241 c << 'debug bgp events' 242 c << 'log file {0}/bgpd.log'.format(self.SHARED_VOLUME) 243 244 with open('{0}/bgpd.conf'.format(self.config_dir), 'w') as f: 245 print colors.yellow('[{0}\'s new bgpd.conf]'.format(self.name)) 246 print colors.yellow(indent(str(c))) 247 f.writelines(str(c)) 248 249 def _create_config_zebra(self): 250 c = CmdBuffer() 251 c << 'hostname zebra' 252 c << 'password zebra' 253 c << 'log file {0}/zebra.log'.format(self.SHARED_VOLUME) 254 c << 'debug zebra packet' 255 c << 'debug zebra kernel' 256 c << 'debug zebra rib' 257 c << 'ipv6 forwarding' 258 c << '' 259 260 with open('{0}/zebra.conf'.format(self.config_dir), 'w') as f: 261 print colors.yellow('[{0}\'s new zebra.conf]'.format(self.name)) 262 print colors.yellow(indent(str(c))) 263 f.writelines(str(c)) 264 265 def vtysh(self, cmd, config=True): 266 if not isinstance(cmd, list): 267 cmd = [cmd] 268 cmd = ' '.join("-c '{0}'".format(c) for c in cmd) 269 if config: 270 return self.local("vtysh -d bgpd -c 'enable' -c 'conf t' -c 'router bgp {0}' {1}".format(self.asn, cmd), capture=True) 271 else: 272 return self.local("vtysh -d bgpd {0}".format(cmd), capture=True) 273 274 def reload_config(self): 275 for daemon in self._get_enabled_daemons(): 276 self.local('pkill -SIGHUP {0}'.format(daemon), capture=True) 277 self._wait_for_boot() 278 279 def _vtysh_add_route_map(self, path): 280 supported_attributes = ( 281 'next-hop', 282 'as-path', 283 'community', 284 'med', 285 'local-pref', 286 'extended-community', 287 ) 288 if not any([path[k] for k in supported_attributes]): 289 return '' 290 291 c = CmdBuffer(' ') 292 route_map_name = 'RM-{0}'.format(path['prefix']) 293 c << "vtysh -c 'configure terminal'" 294 c << "-c 'route-map {0} permit 10'".format(route_map_name) 295 if path['next-hop']: 296 if path['rf'] == 'ipv4': 297 c << "-c 'set ip next-hop {0}'".format(path['next-hop']) 298 elif path['rf'] == 'ipv6': 299 c << "-c 'set ipv6 next-hop {0}'".format(path['next-hop']) 300 else: 301 raise ValueError('Unsupported address family: {0}'.format(path['rf'])) 302 if path['as-path']: 303 as_path = ' '.join([str(n) for n in path['as-path']]) 304 c << "-c 'set as-path prepend {0}'".format(as_path) 305 if path['community']: 306 comm = ' '.join(path['community']) 307 c << "-c 'set community {0}'".format(comm) 308 if path['med']: 309 c << "-c 'set metric {0}'".format(path['med']) 310 if path['local-pref']: 311 c << "-c 'set local-preference {0}'".format(path['local-pref']) 312 if path['extended-community']: 313 # Note: Currently only RT is supported. 314 extcomm = ' '.join(path['extended-community']) 315 c << "-c 'set extcommunity rt {0}'".format(extcomm) 316 self.local(str(c), capture=True) 317 318 return route_map_name 319 320 def add_route(self, route, rf='ipv4', attribute=None, aspath=None, 321 community=None, med=None, extendedcommunity=None, 322 nexthop=None, matchs=None, thens=None, 323 local_pref=None, identifier=None, reload_config=False): 324 if not self._is_running(): 325 raise RuntimeError('Quagga/Zebra is not yet running') 326 327 if rf not in ('ipv4', 'ipv6'): 328 raise ValueError('Unsupported address family: {0}'.format(rf)) 329 330 self.routes.setdefault(route, []) 331 path = { 332 'prefix': route, 333 'rf': rf, 334 'next-hop': nexthop, 335 'as-path': aspath, 336 'community': community, 337 'med': med, 338 'local-pref': local_pref, 339 'extended-community': extendedcommunity, 340 # Note: The following settings are not yet supported on this 341 # implementation. 342 'attr': None, 343 'identifier': None, 344 'matchs': None, 345 'thens': None, 346 } 347 348 # Prepare route-map before adding prefix 349 route_map_name = self._vtysh_add_route_map(path) 350 path['route_map'] = route_map_name 351 352 c = CmdBuffer(' ') 353 c << "vtysh -c 'configure terminal'" 354 c << "-c 'router bgp {0}'".format(self.asn) 355 if rf == 'ipv6': 356 c << "-c 'address-family ipv6'" 357 if route_map_name: 358 c << "-c 'network {0} route-map {1}'".format(route, route_map_name) 359 else: 360 c << "-c 'network {0}'".format(route) 361 self.local(str(c), capture=True) 362 363 self.routes[route].append(path) 364 365 def _vtysh_del_route_map(self, path): 366 route_map_name = path.get('route_map', '') 367 if not route_map_name: 368 return 369 370 c = CmdBuffer(' ') 371 c << "vtysh -c 'configure terminal'" 372 c << "-c 'no route-map {0}'".format(route_map_name) 373 self.local(str(c), capture=True) 374 375 def del_route(self, route, identifier=None, reload_config=False): 376 if not self._is_running(): 377 raise RuntimeError('Quagga/Zebra is not yet running') 378 379 path = None 380 new_paths = [] 381 for p in self.routes.get(route, []): 382 if p['identifier'] != identifier: 383 new_paths.append(p) 384 else: 385 path = p 386 if not path: 387 return 388 389 rf = path['rf'] 390 c = CmdBuffer(' ') 391 c << "vtysh -c 'configure terminal'" 392 c << "-c 'router bgp {0}'".format(self.asn) 393 c << "-c 'address-family {0} unicast'".format(rf) 394 c << "-c 'no network {0}'".format(route) 395 self.local(str(c), capture=True) 396 397 # Delete route-map after deleting prefix 398 self._vtysh_del_route_map(path) 399 400 self.routes[route] = new_paths 401 402 403 class RawQuaggaBGPContainer(QuaggaBGPContainer): 404 def __init__(self, name, config, ctn_image_name='osrg/quagga', zebra=False): 405 asn = None 406 router_id = None 407 for line in config.split('\n'): 408 line = line.strip() 409 if line.startswith('router bgp'): 410 asn = int(line[len('router bgp'):].strip()) 411 if line.startswith('bgp router-id'): 412 router_id = line[len('bgp router-id'):].strip() 413 if not asn: 414 raise Exception('asn not in quagga config') 415 if not router_id: 416 raise Exception('router-id not in quagga config') 417 self.config = config 418 super(RawQuaggaBGPContainer, self).__init__(name, asn, router_id, 419 ctn_image_name, zebra) 420 421 def create_config(self): 422 with open('{0}/bgpd.conf'.format(self.config_dir), 'w') as f: 423 print colors.yellow('[{0}\'s new bgpd.conf]'.format(self.name)) 424 print colors.yellow(indent(self.config)) 425 f.writelines(self.config) 426 427 428 class QuaggaOSPFContainer(OSPFContainer): 429 SHARED_VOLUME = '/etc/quagga' 430 ZAPI_V2_IMAGE = 'osrg/quagga' 431 ZAPI_V3_IMAGE = 'osrg/quagga:v1.0' 432 433 def __init__(self, name, image=ZAPI_V2_IMAGE, zapi_verion=2, 434 zebra_config=None, ospfd_config=None): 435 if zapi_verion != 2: 436 image = self.ZAPI_V3_IMAGE 437 super(QuaggaOSPFContainer, self).__init__(name, image) 438 self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) 439 440 self.zapi_vserion = zapi_verion 441 442 # Example: 443 # zebra_config = { 444 # 'interfaces': { # interface settings 445 # 'eth0': [ 446 # 'ip address 192.168.0.1/24', 447 # ], 448 # }, 449 # 'routes': [ # static route settings 450 # 'ip route 172.16.0.0/16 172.16.0.1', 451 # ], 452 # } 453 self.zebra_config = zebra_config or {} 454 455 # Example: 456 # ospfd_config = { 457 # 'redistribute_types': [ 458 # 'connected', 459 # ], 460 # 'networks': { 461 # '192.168.1.0/24': '0.0.0.0', # <network>: <area> 462 # }, 463 # } 464 self.ospfd_config = ospfd_config or {} 465 466 def run(self): 467 super(QuaggaOSPFContainer, self).run() 468 # self.create_config() is called in super(...).run() 469 self._start_zebra() 470 self._start_ospfd() 471 return self.WAIT_FOR_BOOT 472 473 def create_config(self): 474 self._create_config_zebra() 475 self._create_config_ospfd() 476 477 def _create_config_zebra(self): 478 c = CmdBuffer() 479 c << 'hostname zebra' 480 c << 'password zebra' 481 for name, settings in self.zebra_config.get('interfaces', {}).items(): 482 c << 'interface {0}'.format(name) 483 for setting in settings: 484 c << str(setting) 485 for route in self.zebra_config.get('routes', []): 486 c << str(route) 487 c << 'log file {0}/zebra.log'.format(self.SHARED_VOLUME) 488 c << 'debug zebra packet' 489 c << 'debug zebra kernel' 490 c << 'debug zebra rib' 491 c << 'ipv6 forwarding' 492 c << '' 493 494 with open('{0}/zebra.conf'.format(self.config_dir), 'w') as f: 495 print colors.yellow('[{0}\'s new zebra.conf]'.format(self.name)) 496 print colors.yellow(indent(str(c))) 497 f.writelines(str(c)) 498 499 def _create_config_ospfd(self): 500 c = CmdBuffer() 501 c << 'hostname ospfd' 502 c << 'password zebra' 503 c << 'router ospf' 504 for redistribute in self.ospfd_config.get('redistributes', []): 505 c << ' redistribute {0}'.format(redistribute) 506 for network, area in self.ospfd_config.get('networks', {}).items(): 507 self.networks[network] = area # for superclass 508 c << ' network {0} area {1}'.format(network, area) 509 c << 'log file {0}/ospfd.log'.format(self.SHARED_VOLUME) 510 c << '' 511 512 with open('{0}/ospfd.conf'.format(self.config_dir), 'w') as f: 513 print colors.yellow('[{0}\'s new ospfd.conf]'.format(self.name)) 514 print colors.yellow(indent(str(c))) 515 f.writelines(str(c)) 516 517 def _start_zebra(self): 518 # Do nothing. supervisord will automatically start Zebra daemon. 519 return 520 521 def _start_ospfd(self): 522 if self.zapi_vserion == 2: 523 ospfd_cmd = '/usr/lib/quagga/ospfd' 524 else: 525 ospfd_cmd = 'ospfd' 526 self.local( 527 '{0} -f {1}/ospfd.conf'.format(ospfd_cmd, self.SHARED_VOLUME), 528 detach=True)