github.com/osrg/gobgp/v3@v3.30.0/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 17 18 import re 19 import netaddr 20 21 from lib.base import ( 22 wait_for_completion, 23 BGPContainer, 24 OSPFContainer, 25 CmdBuffer, 26 BGP_FSM_IDLE, 27 BGP_FSM_ACTIVE, 28 BGP_FSM_ESTABLISHED, 29 BGP_ATTR_TYPE_MULTI_EXIT_DISC, 30 BGP_ATTR_TYPE_LOCAL_PREF, 31 yellow, 32 indent, 33 ) 34 35 36 class QuaggaBGPContainer(BGPContainer): 37 38 WAIT_FOR_BOOT = 1 39 SHARED_VOLUME = '/etc/quagga' 40 41 def __init__(self, name, asn, router_id, ctn_image_name='osrg/quagga', bgpd_config=None, zebra=False): 42 super(QuaggaBGPContainer, self).__init__(name, asn, router_id, 43 ctn_image_name) 44 self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) 45 self.zebra = zebra 46 47 # bgp_config is equivalent to config.BgpConfigSet structure 48 # Example: 49 # bgpd_config = { 50 # 'global': { 51 # 'confederation': { 52 # 'identifier': 10, 53 # 'peers': [65001], 54 # }, 55 # }, 56 # } 57 self.bgpd_config = bgpd_config or {} 58 59 def _get_enabled_daemons(self): 60 daemons = ['bgpd'] 61 if self.zebra: 62 daemons.append('zebra') 63 return daemons 64 65 def _is_running(self): 66 def f(d): 67 return self.local( 68 'vtysh -d {0} -c "show version"' 69 ' > /dev/null 2>&1; echo $?'.format(d), capture=True) == '0' 70 71 return all([f(d) for d in self._get_enabled_daemons()]) 72 73 def _wait_for_boot(self): 74 wait_for_completion(self._is_running) 75 76 def run(self): 77 super(QuaggaBGPContainer, self).run() 78 self._wait_for_boot() 79 return self.WAIT_FOR_BOOT 80 81 def get_global_rib(self, prefix='', rf='ipv4'): 82 rib = [] 83 if prefix != '': 84 return self.get_global_rib_with_prefix(prefix, rf) 85 86 out = self.vtysh('show bgp {0} unicast'.format(rf), config=False) 87 if out.startswith('No BGP network exists'): 88 return rib 89 90 for line in out.split('\n')[6:-2]: 91 line = line[3:] 92 93 p = line.split()[0] 94 if '/' not in p: 95 continue 96 97 rib.extend(self.get_global_rib_with_prefix(p, rf)) 98 99 return rib 100 101 def get_global_rib_with_prefix(self, prefix, rf): 102 rib = [] 103 104 lines = [line.strip() for line in self.vtysh('show bgp {0} unicast {1}'.format(rf, prefix), config=False).split('\n')] 105 106 if lines[0] == '% Network not in table': 107 return rib 108 109 lines = lines[2:] 110 111 if lines[0].startswith('Not advertised'): 112 lines.pop(0) # another useless line 113 elif lines[0].startswith('Advertised to non peer-group peers:'): 114 lines = lines[2:] # other useless lines 115 else: 116 raise Exception('unknown output format {0}'.format(lines)) 117 118 while len(lines) > 0: 119 if lines[0] == 'Local': 120 aspath = [] 121 else: 122 aspath = [int(re.sub('\D', '', asn)) for asn in lines[0].split()] 123 124 nexthop = lines[1].split()[0].strip() 125 info = [s.strip(',') for s in lines[2].split()] 126 attrs = [] 127 ibgp = False 128 best = False 129 if 'metric' in info: 130 med = info[info.index('metric') + 1] 131 attrs.append({'type': BGP_ATTR_TYPE_MULTI_EXIT_DISC, 'metric': int(med)}) 132 if 'localpref' in info: 133 localpref = info[info.index('localpref') + 1] 134 attrs.append({'type': BGP_ATTR_TYPE_LOCAL_PREF, 'value': int(localpref)}) 135 if 'internal' in info: 136 ibgp = True 137 if 'best' in info: 138 best = True 139 140 rib.append({'prefix': prefix, 'nexthop': nexthop, 141 'aspath': aspath, 'attrs': attrs, 'ibgp': ibgp, 'best': best}) 142 143 lines = lines[5:] 144 145 return rib 146 147 def get_neighbor_state(self, peer): 148 if peer not in self.peers: 149 raise Exception('not found peer {0}'.format(peer.router_id)) 150 151 neigh_addr = self.peers[peer]['neigh_addr'].split('/')[0] 152 153 info = [l.strip() for l in self.vtysh('show bgp neighbors {0}'.format(neigh_addr), config=False).split('\n')] 154 155 if not info[0].startswith('BGP neighbor is'): 156 raise Exception('unknown format') 157 158 idx1 = info[0].index('BGP neighbor is ') 159 idx2 = info[0].index(',') 160 n_addr = info[0][idx1 + len('BGP neighbor is '):idx2] 161 if n_addr == neigh_addr: 162 idx1 = info[2].index('= ') 163 state = info[2][idx1 + len('= '):] 164 if state.startswith('Idle'): 165 return BGP_FSM_IDLE 166 elif state.startswith('Active'): 167 return BGP_FSM_ACTIVE 168 elif state.startswith('Established'): 169 return BGP_FSM_ESTABLISHED 170 else: 171 return state 172 173 raise Exception('not found peer {0}'.format(peer.router_id)) 174 175 def send_route_refresh(self): 176 self.vtysh('clear ip bgp * soft', config=False) 177 178 def create_config(self): 179 self._create_config_bgp() 180 if self.zebra: 181 self._create_config_zebra() 182 183 def _create_config_bgp(self): 184 185 c = CmdBuffer() 186 c << 'hostname bgpd' 187 c << 'password zebra' 188 c << 'router bgp {0}'.format(self.asn) 189 c << 'bgp router-id {0}'.format(self.router_id) 190 if any(info['graceful_restart'] for info in self.peers.values()): 191 c << 'bgp graceful-restart' 192 193 if 'global' in self.bgpd_config: 194 if 'confederation' in self.bgpd_config['global']: 195 conf = self.bgpd_config['global']['confederation']['config'] 196 c << 'bgp confederation identifier {0}'.format(conf['identifier']) 197 c << 'bgp confederation peers {0}'.format(' '.join([str(i) for i in conf['member-as-list']])) 198 199 version = 4 200 for peer, info in self.peers.items(): 201 version = netaddr.IPNetwork(info['neigh_addr']).version 202 n_addr = info['neigh_addr'].split('/')[0] 203 if version == 6: 204 c << 'no bgp default ipv4-unicast' 205 c << 'neighbor {0} remote-as {1}'.format(n_addr, info['remote_as']) 206 # For rapid convergence 207 c << 'neighbor {0} advertisement-interval 1'.format(n_addr) 208 if info['is_rs_client']: 209 c << 'neighbor {0} route-server-client'.format(n_addr) 210 for typ, p in info['policies'].items(): 211 c << 'neighbor {0} route-map {1} {2}'.format(n_addr, p['name'], 212 typ) 213 if info['passwd']: 214 c << 'neighbor {0} password {1}'.format(n_addr, info['passwd']) 215 if info['passive']: 216 c << 'neighbor {0} passive'.format(n_addr) 217 if version == 6: 218 c << 'address-family ipv6 unicast' 219 c << 'neighbor {0} activate'.format(n_addr) 220 c << 'exit-address-family' 221 222 if self.zebra: 223 if version == 6: 224 c << 'address-family ipv6 unicast' 225 c << 'redistribute connected' 226 c << 'exit-address-family' 227 else: 228 c << 'redistribute connected' 229 230 for name, policy in self.policies.items(): 231 c << 'access-list {0} {1} {2}'.format(name, policy['type'], 232 policy['match']) 233 c << 'route-map {0} permit 10'.format(name) 234 c << 'match ip address {0}'.format(name) 235 c << 'set metric {0}'.format(policy['med']) 236 237 c << 'debug bgp as4' 238 c << 'debug bgp fsm' 239 c << 'debug bgp updates' 240 c << 'debug bgp events' 241 c << 'log file {0}/bgpd.log'.format(self.SHARED_VOLUME) 242 243 with open('{0}/bgpd.conf'.format(self.config_dir), 'w') as f: 244 print(yellow('[{0}\'s new bgpd.conf]'.format(self.name))) 245 print(yellow(indent(str(c)))) 246 f.writelines(str(c)) 247 248 def _create_config_zebra(self): 249 c = CmdBuffer() 250 c << 'hostname zebra' 251 c << 'password zebra' 252 c << 'log file {0}/zebra.log'.format(self.SHARED_VOLUME) 253 c << 'debug zebra packet' 254 c << 'debug zebra kernel' 255 c << 'debug zebra rib' 256 c << 'ipv6 forwarding' 257 c << '' 258 259 with open('{0}/zebra.conf'.format(self.config_dir), 'w') as f: 260 print(yellow('[{0}\'s new zebra.conf]'.format(self.name))) 261 c = str(c).strip() 262 print(yellow(indent(c))) 263 f.writelines(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(yellow('[{0}\'s new bgpd.conf]'.format(self.name))) 424 print(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 list(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(yellow('[{0}\'s new zebra.conf]'.format(self.name))) 496 c = str(c).strip() 497 print(yellow(indent(c))) 498 f.writelines(c) 499 500 def _create_config_ospfd(self): 501 c = CmdBuffer() 502 c << 'hostname ospfd' 503 c << 'password zebra' 504 c << 'router ospf' 505 for redistribute in self.ospfd_config.get('redistributes', []): 506 c << ' redistribute {0}'.format(redistribute) 507 for network, area in list(self.ospfd_config.get('networks', {}).items()): 508 self.networks[network] = area # for superclass 509 c << ' network {0} area {1}'.format(network, area) 510 c << 'log file {0}/ospfd.log'.format(self.SHARED_VOLUME) 511 c << '' 512 513 with open('{0}/ospfd.conf'.format(self.config_dir), 'w') as f: 514 print(yellow('[{0}\'s new ospfd.conf]'.format(self.name))) 515 print(yellow(indent(str(c)))) 516 f.writelines(str(c)) 517 518 def _start_zebra(self): 519 # Do nothing. supervisord will automatically start Zebra daemon. 520 return 521 522 def _start_ospfd(self): 523 if self.zapi_vserion == 2: 524 ospfd_cmd = '/usr/lib/quagga/ospfd' 525 else: 526 ospfd_cmd = 'ospfd' 527 self.local( 528 '{0} -f {1}/ospfd.conf'.format(ospfd_cmd, self.SHARED_VOLUME), 529 detach=True)