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)