github.com/osrg/gobgp/v3@v3.30.0/test/lib/base.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  import os
    18  import time
    19  import itertools
    20  
    21  from invoke import run
    22  
    23  import textwrap
    24  from colored import fg, attr
    25  
    26  try:
    27      from docker import Client
    28  except ImportError:
    29      from docker import APIClient as Client
    30  import netaddr
    31  
    32  
    33  DEFAULT_TEST_PREFIX = ''
    34  DEFAULT_TEST_BASE_DIR = '/tmp/gobgp'
    35  TEST_PREFIX = DEFAULT_TEST_PREFIX
    36  TEST_BASE_DIR = DEFAULT_TEST_BASE_DIR
    37  
    38  BGP_FSM_IDLE = 'idle'
    39  BGP_FSM_ACTIVE = 'active'
    40  BGP_FSM_ESTABLISHED = 'established'
    41  
    42  BGP_ATTR_TYPE_ORIGIN = 1
    43  BGP_ATTR_TYPE_AS_PATH = 2
    44  BGP_ATTR_TYPE_NEXT_HOP = 3
    45  BGP_ATTR_TYPE_MULTI_EXIT_DISC = 4
    46  BGP_ATTR_TYPE_LOCAL_PREF = 5
    47  BGP_ATTR_TYPE_COMMUNITIES = 8
    48  BGP_ATTR_TYPE_ORIGINATOR_ID = 9
    49  BGP_ATTR_TYPE_CLUSTER_LIST = 10
    50  BGP_ATTR_TYPE_MP_REACH_NLRI = 14
    51  BGP_ATTR_TYPE_EXTENDED_COMMUNITIES = 16
    52  
    53  GRACEFUL_RESTART_TIME = 30
    54  LONG_LIVED_GRACEFUL_RESTART_TIME = 30
    55  
    56  FLOWSPEC_NAME_TO_TYPE = {
    57      "destination": 1,
    58      "source": 2,
    59      "protocol": 3,
    60      "port": 4,
    61      "destination-port": 5,
    62      "source-port": 6,
    63      "icmp-type": 7,
    64      "icmp-code": 8,
    65      "tcp-flags": 9,
    66      "packet-length": 10,
    67      "dscp": 11,
    68      "fragment": 12,
    69      "label": 13,
    70      "ether-type": 14,
    71      "source-mac": 15,
    72      "destination-mac": 16,
    73      "llc-dsap": 17,
    74      "llc-ssap": 18,
    75      "llc-control": 19,
    76      "snap": 20,
    77      "vid": 21,
    78      "cos": 22,
    79      "inner-vid": 23,
    80      "inner-cos": 24,
    81  }
    82  
    83  # with this label, we can do filtering in `docker ps` and `docker network prune`
    84  TEST_CONTAINER_LABEL = 'gobgp-test'
    85  TEST_NETWORK_LABEL = TEST_CONTAINER_LABEL
    86  
    87  
    88  def local(s, capture=False):
    89      print('[localhost] local:', s)
    90      _env = {'NOSE_NOLOGCAPTURE': '1' if capture else '0'}
    91      return run(s, hide=True, env=_env).stdout.strip()
    92  
    93  
    94  def yellow(s):
    95      return fg('yellow') + str(s) + attr('reset')
    96  
    97  
    98  def indent(s):
    99      return textwrap.indent(str(s), ' '*4, lambda line: True)
   100  
   101  
   102  def community_str(i):
   103      """
   104      Converts integer in to colon separated two bytes decimal strings like
   105      BGP Community or Large Community representation.
   106  
   107      For example, this function converts 13107300 = ((200 << 16) | 100)
   108      into "200:100".
   109      """
   110      values = []
   111      while i > 0:
   112          values.append(str(i & 0xffff))
   113          i >>= 16
   114      return ':'.join(reversed(values))
   115  
   116  
   117  def wait_for_completion(f, timeout=120):
   118      interval = 1
   119      count = 0
   120      while True:
   121          if f():
   122              return
   123  
   124          time.sleep(interval)
   125          count += interval
   126          if count >= timeout:
   127              raise Exception('timeout')
   128  
   129  
   130  def try_several_times(f, t=3, s=1):
   131      for _ in range(t):
   132          try:
   133              r = f()
   134          except RuntimeError:
   135              time.sleep(s)
   136          else:
   137              return r
   138      raise Exception
   139  
   140  
   141  def assert_several_times(f, t=30, s=1):
   142      e = AssertionError
   143      for _ in range(t):
   144          try:
   145              f()
   146          except AssertionError as ae:
   147              time.sleep(s)
   148              e = ae
   149          else:
   150              return
   151      raise e
   152  
   153  
   154  def get_bridges():
   155      return try_several_times(lambda: local("docker network ls | awk 'NR > 1{print $2}'", capture=True)).split('\n')
   156  
   157  
   158  def get_containers():
   159      return try_several_times(lambda: local("docker ps -a | awk 'NR > 1 {print $NF}'", capture=True)).split('\n')
   160  
   161  
   162  class CmdBuffer(list):
   163      def __init__(self, delim='\n'):
   164          super(CmdBuffer, self).__init__()
   165          self.delim = delim
   166  
   167      def __lshift__(self, value):
   168          self.append(value)
   169  
   170      def __str__(self):
   171          return self.delim.join(self)
   172  
   173  
   174  class Bridge(object):
   175      def __init__(self, name, subnet='', with_ip=True, self_ip=False):
   176          self.name = name
   177          if TEST_PREFIX != '':
   178              self.name = '{0}_{1}'.format(TEST_PREFIX, name)
   179          self.with_ip = with_ip
   180          if with_ip:
   181              self.subnet = netaddr.IPNetwork(subnet)
   182  
   183              def _f():
   184                  for host in self.subnet:
   185                      yield host
   186              self._ip_generator = _f()
   187              # throw away first network address
   188              self.next_ip_address()
   189  
   190          def f():
   191              if self.name in get_bridges():
   192                  self.delete()
   193              v6 = ''
   194              if self.subnet.version == 6:
   195                  v6 = '--ipv6'
   196              self.id = local('docker network create --driver bridge {0} --subnet {1} --label {2} {3}'.format(v6, subnet, TEST_NETWORK_LABEL, self.name), capture=True)
   197          try_several_times(f)
   198  
   199          self.self_ip = self_ip
   200          if self_ip:
   201              self.ip_addr = self.next_ip_address()
   202              try_several_times(lambda: local("ip addr add {0} dev {1}".format(self.ip_addr, self.name)))
   203          self.ctns = []
   204  
   205          # Note: Here removes routes from the container host to prevent traffic
   206          # from going through the container host's routing table.
   207          if with_ip:
   208              local('ip route del {0}; echo $?'.format(subnet),
   209                    capture=True)
   210              # When IPv6, 2 routes will be installed to the container host's
   211              # routing table.
   212              if self.subnet.version == 6:
   213                  local('ip -6 route del {0}; echo $?'.format(subnet),
   214                        capture=True)
   215  
   216      def next_ip_address(self):
   217          return "{0}/{1}".format(next(self._ip_generator),
   218                                  self.subnet.prefixlen)
   219  
   220      def addif(self, ctn, ip_addr=''):
   221          _name = ctn.next_if_name()
   222          self.ctns.append(ctn)
   223          ip = ''
   224          if not ip_addr == '':
   225              ip = '--ip {0}'.format(ip_addr)
   226              if self.subnet.version == 6:
   227                  ip = '--ip6 {0}'.format(ip_addr)
   228          local("docker network connect {0} {1} {2}".format(ip, self.name, ctn.docker_name()))
   229          i = [x for x in list(Client(timeout=60, version='auto').inspect_network(self.id)['Containers'].values()) if x['Name'] == ctn.docker_name()][0]
   230          if self.subnet.version == 4:
   231              eth = 'eth{0}'.format(len(ctn.ip_addrs))
   232              addr = i['IPv4Address']
   233              ctn.ip_addrs.append((eth, addr, self.name))
   234          else:
   235              eth = 'eth{0}'.format(len(ctn.ip6_addrs))
   236              addr = i['IPv6Address']
   237              ctn.ip6_addrs.append((eth, addr, self.name))
   238  
   239      def delete(self):
   240          try_several_times(lambda: local("docker network rm {0}".format(self.name)))
   241  
   242  
   243  class Container(object):
   244      def __init__(self, name, image):
   245          self.name = name
   246          self.image = image
   247          self.shared_volumes = []
   248          self.ip_addrs = []
   249          self.ip6_addrs = []
   250          self.is_running = False
   251          self.eths = []
   252          self.tcpdump_running = False
   253  
   254          if self.docker_name() in get_containers():
   255              self.remove()
   256  
   257      def docker_name(self):
   258          if TEST_PREFIX == DEFAULT_TEST_PREFIX:
   259              return '{0}'.format(self.name)
   260          return '{0}_{1}'.format(TEST_PREFIX, self.name)
   261  
   262      def next_if_name(self):
   263          name = 'eth{0}'.format(len(self.eths) + 1)
   264          self.eths.append(name)
   265          return name
   266  
   267      def run(self):
   268          c = CmdBuffer(' ')
   269          c << "docker run --privileged=true"
   270          for sv in self.shared_volumes:
   271              c << "-v {0}:{1}".format(sv[0], sv[1])
   272          c << "--name {0} -l {1} -id {2}".format(self.docker_name(), TEST_CONTAINER_LABEL, self.image)
   273          self.id = try_several_times(lambda: local(str(c), capture=True))
   274          self.is_running = True
   275          self.local("ip li set up dev lo")
   276          for line in self.local("ip a show dev eth0", capture=True).split('\n'):
   277              if line.strip().startswith("inet "):
   278                  elems = [e.strip() for e in line.strip().split(' ')]
   279                  self.ip_addrs.append(('eth0', elems[1], 'docker0'))
   280              elif line.strip().startswith("inet6 "):
   281                  elems = [e.strip() for e in line.strip().split(' ')]
   282                  self.ip6_addrs.append(('eth0', elems[1], 'docker0'))
   283          return 0
   284  
   285      def stop(self):
   286          ret = try_several_times(lambda: local("docker stop -t 0 " + self.docker_name(), capture=True))
   287          self.is_running = False
   288          return ret
   289  
   290      def remove(self):
   291          ret = try_several_times(lambda: local("docker rm -f " + self.docker_name(), capture=True))
   292          self.is_running = False
   293          return ret
   294  
   295      def local(self, cmd, capture=False, stream=False, detach=False, tty=True):
   296          if stream:
   297              dckr = Client(timeout=120, version='auto')
   298              i = dckr.exec_create(container=self.docker_name(), cmd=cmd)
   299              return dckr.exec_start(i['Id'], tty=tty, stream=stream, detach=detach)
   300          else:
   301              flag = '-d' if detach else ''
   302              return local('docker exec {0} {1} {2}'.format(flag, self.docker_name(), cmd), capture)
   303  
   304      def get_pid(self):
   305          if self.is_running:
   306              cmd = "docker inspect -f '{{.State.Pid}}' " + self.docker_name()
   307              return int(local(cmd, capture=True))
   308          return -1
   309  
   310      def start_tcpdump(self, interface=None, filename=None, expr='tcp port 179'):
   311          if self.tcpdump_running:
   312              raise Exception('tcpdump already running')
   313          self.tcpdump_running = True
   314          if not interface:
   315              interface = "eth0"
   316          if not filename:
   317              filename = '{0}.dump'.format(interface)
   318          self.local("tcpdump -U -i {0} -w {1}/{2} {3}".format(interface, self.shared_volumes[0][1], filename, expr), detach=True)
   319          return '{0}/{1}'.format(self.shared_volumes[0][0], filename)
   320  
   321      def stop_tcpdump(self):
   322          self.local("pkill tcpdump")
   323          self.tcpdump_running = False
   324  
   325  
   326  class BGPContainer(Container):
   327  
   328      WAIT_FOR_BOOT = 1
   329      RETRY_INTERVAL = 5
   330  
   331      def __init__(self, name, asn, router_id, ctn_image_name):
   332          self.config_dir = '/'.join((TEST_BASE_DIR, TEST_PREFIX, name))
   333          local('if [ -e {0} ]; then rm -rf {0}; fi'.format(self.config_dir))
   334          local('mkdir -p {0}'.format(self.config_dir))
   335          local('chmod 777 {0}'.format(self.config_dir))
   336          self.asn = asn
   337          self.router_id = router_id
   338          self.peers = {}
   339          self.routes = {}
   340          self.policies = {}
   341          super(BGPContainer, self).__init__(name, ctn_image_name)
   342  
   343      def __repr__(self):
   344          return str({'name': self.name, 'asn': self.asn, 'router_id': self.router_id})
   345  
   346      def run(self):
   347          self.create_config()
   348          super(BGPContainer, self).run()
   349          return self.WAIT_FOR_BOOT
   350  
   351      def peer_name(self, peer):
   352          if peer not in self.peers:
   353              raise Exception('not found peer {0}'.format(peer.router_id))
   354          name = self.peers[peer]['interface']
   355          if name == '':
   356              name = self.peers[peer]['neigh_addr'].split('/')[0]
   357          return name
   358  
   359      def update_peer(self, peer, **kwargs):
   360          if peer not in self.peers:
   361              raise Exception('peer not exists')
   362          self.add_peer(peer, **kwargs)
   363  
   364      def add_peer(
   365          self,
   366          peer,
   367          passwd=None,
   368          vpn=False,
   369          is_rs_client=False,
   370          policies=None,
   371          passive=False,
   372          is_rr_client=False,
   373          cluster_id=None,
   374          flowspec=False,
   375          bridge="",
   376          reload_config=True,
   377          as2=False,
   378          graceful_restart=None,
   379          local_as=None,
   380          prefix_limit=None,
   381          v6=False,
   382          llgr=None,
   383          vrf="",
   384          interface="",
   385          allow_as_in=0,
   386          remove_private_as=None,
   387          replace_peer_as=False,
   388          addpath=0,
   389          treat_as_withdraw=False,
   390          remote_as=None,
   391          mup=False,
   392      ):
   393          neigh_addr = ''
   394          local_addr = ''
   395          it = itertools.product(self.ip_addrs, peer.ip_addrs)
   396          if v6:
   397              it = itertools.product(self.ip6_addrs, peer.ip6_addrs)
   398  
   399          if interface == '':
   400              for me, you in it:
   401                  if bridge != '' and bridge != me[2]:
   402                      continue
   403                  if me[2] == you[2]:
   404                      neigh_addr = you[1]
   405                      local_addr = me[1]
   406                      if v6:
   407                          addr, mask = local_addr.split('/')
   408                          local_addr = "{0}%{1}/{2}".format(addr, me[0], mask)
   409                      break
   410  
   411              if neigh_addr == '':
   412                  raise Exception('peer {0} seems not ip reachable'.format(peer))
   413  
   414          if not policies:
   415              policies = {}
   416  
   417          self.peers[peer] = {'neigh_addr': neigh_addr,
   418                              'interface': interface,
   419                              'passwd': passwd,
   420                              'vpn': vpn,
   421                              'flowspec': flowspec,
   422                              'is_rs_client': is_rs_client,
   423                              'is_rr_client': is_rr_client,
   424                              'cluster_id': cluster_id,
   425                              'policies': policies,
   426                              'passive': passive,
   427                              'local_addr': local_addr,
   428                              'as2': as2,
   429                              'graceful_restart': graceful_restart,
   430                              'local_as': local_as,
   431                              'prefix_limit': prefix_limit,
   432                              'llgr': llgr,
   433                              'vrf': vrf,
   434                              'allow_as_in': allow_as_in,
   435                              'remove_private_as': remove_private_as,
   436                              'replace_peer_as': replace_peer_as,
   437                              'addpath': addpath,
   438                              'treat_as_withdraw': treat_as_withdraw,
   439                              'remote_as': remote_as or peer.asn,
   440                              'mup': mup}
   441          if self.is_running and reload_config:
   442              self.create_config()
   443              self.reload_config()
   444  
   445      def del_peer(self, peer, reload_config=True):
   446          del self.peers[peer]
   447          if self.is_running and reload_config:
   448              self.create_config()
   449              self.reload_config()
   450  
   451      def disable_peer(self, peer):
   452          raise Exception('implement disable_peer() method')
   453  
   454      def enable_peer(self, peer):
   455          raise Exception('implement enable_peer() method')
   456  
   457      def log(self):
   458          return local('cat {0}/*.log'.format(self.config_dir), capture=True)
   459  
   460      def _extract_routes(self, families):
   461          routes = {}
   462          for prefix, paths in list(self.routes.items()):
   463              if paths and paths[0]['rf'] in families:
   464                  routes[prefix] = paths
   465          return routes
   466  
   467      def add_route(self, route, rf='ipv4', attribute=None, aspath=None,
   468                    community=None, med=None, extendedcommunity=None,
   469                    nexthop=None, matchs=None, thens=None,
   470                    local_pref=None, identifier=None, reload_config=True):
   471          if route not in self.routes:
   472              self.routes[route] = []
   473          prefix = route
   474          if 'flowspec' in rf:
   475              prefix = ' '.join(['match'] + matchs)
   476          self.routes[route].append({
   477              'prefix': prefix,
   478              'rf': rf,
   479              'attr': attribute,
   480              'next-hop': nexthop,
   481              'as-path': aspath,
   482              'community': community,
   483              'med': med,
   484              'local-pref': local_pref,
   485              'extended-community': extendedcommunity,
   486              'identifier': identifier,
   487              'matchs': matchs,
   488              'thens': thens,
   489          })
   490          if self.is_running and reload_config:
   491              self.create_config()
   492              self.reload_config()
   493  
   494      def del_route(self, route, identifier=None, reload_config=True):
   495          if route not in self.routes:
   496              return
   497          self.routes[route] = [p for p in self.routes[route] if p['identifier'] != identifier]
   498          if self.is_running and reload_config:
   499              self.create_config()
   500              self.reload_config()
   501  
   502      def add_policy(self, policy, peer, typ, default='accept', reload_config=True):
   503          self.set_default_policy(peer, typ, default)
   504          self.define_policy(policy)
   505          self.assign_policy(peer, policy, typ)
   506          if self.is_running and reload_config:
   507              self.create_config()
   508              self.reload_config()
   509  
   510      def set_default_policy(self, peer, typ, default):
   511          if typ in ['in', 'out', 'import', 'export'] and default in ['reject', 'accept']:
   512              if 'default-policy' not in self.peers[peer]:
   513                  self.peers[peer]['default-policy'] = {}
   514              self.peers[peer]['default-policy'][typ] = default
   515          else:
   516              raise Exception('wrong type or default')
   517  
   518      def define_policy(self, policy):
   519          self.policies[policy['name']] = policy
   520  
   521      def assign_policy(self, peer, policy, typ):
   522          if peer not in self.peers:
   523              raise Exception('peer {0} not found'.format(peer.name))
   524          name = policy['name']
   525          if name not in self.policies:
   526              raise Exception('policy {0} not found'.format(name))
   527          self.peers[peer]['policies'][typ] = policy
   528  
   529      def get_local_rib(self, peer, rf):
   530          raise Exception('implement get_local_rib() method')
   531  
   532      def get_global_rib(self, rf):
   533          raise Exception('implement get_global_rib() method')
   534  
   535      def get_neighbor_state(self, peer_id):
   536          raise Exception('implement get_neighbor() method')
   537  
   538      def get_reachability(self, prefix, timeout=20):
   539          version = netaddr.IPNetwork(prefix).version
   540          addr = prefix.split('/')[0]
   541          if version == 4:
   542              ping_cmd = 'ping'
   543          elif version == 6:
   544              ping_cmd = 'ping6'
   545          else:
   546              raise Exception('unsupported route family: {0}'.format(version))
   547          cmd = '/bin/bash -c "/bin/{0} -c 1 -w 1 {1} | xargs echo"'.format(ping_cmd, addr)
   548          interval = 1
   549          count = 0
   550          while True:
   551              res = self.local(cmd, capture=True)
   552              print(yellow(res))
   553              if ('1 packets received' in res or '1 received' in res) and '0% packet loss' in res:
   554                  break
   555              time.sleep(interval)
   556              count += interval
   557              if count >= timeout:
   558                  raise Exception('timeout')
   559          return True
   560  
   561      def wait_for(self, expected_state, peer, timeout=120):
   562          interval = 1
   563          count = 0
   564          while True:
   565              state = self.get_neighbor_state(peer)
   566              print(yellow("{0}'s peer {1} state: {2}".format(self.router_id,
   567                                                              peer.router_id,
   568                                                              state)))
   569              if state == expected_state:
   570                  return
   571  
   572              time.sleep(interval)
   573              count += interval
   574              if count >= timeout:
   575                  raise Exception('timeout')
   576  
   577      def add_static_route(self, network, next_hop):
   578          cmd = '/sbin/ip route add {0} via {1}'.format(network, next_hop)
   579          self.local(cmd, capture=True)
   580  
   581      def set_ipv6_forward(self):
   582          cmd = 'sysctl -w net.ipv6.conf.all.forwarding=1'
   583          self.local(cmd, capture=True)
   584  
   585      def create_config(self):
   586          raise Exception('implement create_config() method')
   587  
   588      def reload_config(self):
   589          raise Exception('implement reload_config() method')
   590  
   591  
   592  class OSPFContainer(Container):
   593      WAIT_FOR_BOOT = 1
   594  
   595      def __init__(self, name, ctn_image_name):
   596          self.config_dir = '/'.join((TEST_BASE_DIR, TEST_PREFIX, name))
   597          local('if [ -e {0} ]; then rm -rf {0}; fi'.format(self.config_dir))
   598          local('mkdir -p {0}'.format(self.config_dir))
   599          local('chmod 777 {0}'.format(self.config_dir))
   600  
   601          # Example:
   602          # networks = {
   603          #     '192.168.1.0/24': '0.0.0.0',  # <network>: <area>
   604          # }
   605          self.networks = {}
   606          super(OSPFContainer, self).__init__(name, ctn_image_name)
   607  
   608      def __repr__(self):
   609          return str({'name': self.name, 'networks': self.networks})
   610  
   611      def run(self):
   612          self.create_config()
   613          super(OSPFContainer, self).run()
   614          return self.WAIT_FOR_BOOT
   615  
   616      def create_config(self):
   617          raise NotImplementedError