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