github.com/osrg/gobgp/v3@v3.30.0/test/lib/yabgp.py (about)

     1  # Copyright (C) 2017 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  
    19  import json
    20  import os
    21  
    22  from lib.base import (
    23      FLOWSPEC_NAME_TO_TYPE,
    24      BGPContainer,
    25      CmdBuffer,
    26      try_several_times,
    27      wait_for_completion,
    28      yellow,
    29      indent,
    30      local,
    31  )
    32  
    33  
    34  class YABGPContainer(BGPContainer):
    35  
    36      WAIT_FOR_BOOT = 1
    37      SHARED_VOLUME = '/etc/yabgp'
    38  
    39      def __init__(self, name, asn, router_id,
    40                   ctn_image_name='osrg/yabgp:v0.4.0'):
    41          super(YABGPContainer, self).__init__(name, asn, router_id,
    42                                               ctn_image_name)
    43          self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME))
    44  
    45      def _copy_helper_app(self):
    46          import lib
    47          mod_dir = os.path.dirname(lib.__file__)
    48          local('docker cp {0}/yabgp_helper.py'
    49                ' {1}:/root/'.format(mod_dir, self.docker_name()))
    50  
    51      def _start_yabgp(self):
    52          self.local(
    53              'python /root/yabgp_helper.py'
    54              ' --config-file {0}/yabgp.ini'.format(self.SHARED_VOLUME),
    55              detach=True)
    56  
    57      def _wait_for_boot(self):
    58          return wait_for_completion(self._curl_is_running)
    59  
    60      def run(self):
    61          super(YABGPContainer, self).run()
    62          # self.create_config() is called in super class
    63          self._copy_helper_app()
    64          # To start YABGP, it is required to configure neighbor settings, so
    65          # here does not start YABGP yet.
    66          # self._start_yabgp()
    67          # self._wait_for_boot()
    68          return self.WAIT_FOR_BOOT
    69  
    70      def create_config(self):
    71          # Currently, supports only single peer
    72          c = CmdBuffer('\n')
    73          c << '[DEFAULT]'
    74          c << 'log_dir = {0}'.format(self.SHARED_VOLUME)
    75          c << 'use_stderr = False'
    76          c << '[message]'
    77          c << 'write_disk = True'
    78          c << 'write_dir = {0}/data/bgp/'.format(self.SHARED_VOLUME)
    79          c << 'format = json'
    80  
    81          if self.peers:
    82              info = next(iter(list(self.peers.values())))
    83              remote_as = info['remote_as']
    84              neigh_addr = info['neigh_addr'].split('/')[0]
    85              local_as = info['local_as'] or self.asn
    86              local_addr = info['local_addr'].split('/')[0]
    87              c << '[bgp]'
    88              c << 'afi_safi = ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn'
    89              c << 'remote_as = {0}'.format(remote_as)
    90              c << 'remote_addr = {0}'.format(neigh_addr)
    91              c << 'local_as = {0}'.format(local_as)
    92              c << 'local_addr = {0}'.format(local_addr)
    93  
    94          with open('{0}/yabgp.ini'.format(self.config_dir), 'w') as f:
    95              print(yellow('[{0}\'s new yabgp.ini]'.format(self.name)))
    96              print(yellow(indent(str(c))))
    97              f.writelines(str(c))
    98  
    99      def reload_config(self):
   100          if self.peers == 0:
   101              return
   102  
   103          def _reload():
   104              def _is_running():
   105                  ps = self.local('ps -ef', capture=True)
   106                  running = False
   107                  for line in ps.split('\n'):
   108                      if 'yabgp_helper' in line:
   109                          running = True
   110                  return running
   111  
   112              if _is_running():
   113                  self.local('/usr/bin/pkill -9 python')
   114  
   115              self._start_yabgp()
   116              self._wait_for_boot()
   117              if not _is_running():
   118                  raise RuntimeError()
   119  
   120          try_several_times(_reload)
   121  
   122      def _curl_is_running(self):
   123          c = CmdBuffer(' ')
   124          c << "curl -X GET"
   125          c << "-u admin:admin"
   126          c << "-H 'Content-Type: application/json'"
   127          c << "http://localhost:8801/v1/"
   128          c << "> /dev/null 2>&1; echo $?"
   129          return self.local(str(c), capture=True) == '0'
   130  
   131      def _curl_send_update(self, path, peer):
   132          c = CmdBuffer(' ')
   133          c << "curl -X POST"
   134          c << "-u admin:admin"
   135          c << "-H 'Content-Type: application/json'"
   136          c << "http://localhost:8801/v1/peer/{0}/send/update".format(peer)
   137          c << "-d '{0}'".format(json.dumps(path))
   138          return json.loads(self.local(str(c), capture=True))
   139  
   140      def _construct_ip_unicast_update(self, rf, prefix, nexthop):
   141          # YABGP v0.4.0
   142          #
   143          # IPv4 Unicast:
   144          # curl -X POST \
   145          #      -u admin:admin \
   146          #      -H 'Content-Type: application/json' \
   147          #      http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{
   148          #     "attr": {
   149          #         "1": 0,
   150          #         "2": [
   151          #             [
   152          #                 2,
   153          #                 [
   154          #                     1,
   155          #                     2,
   156          #                     3
   157          #                 ]
   158          #             ]
   159          #         ],
   160          #         "3": "192.0.2.1",
   161          #         "5": 500
   162          #     },
   163          #     "nlri": [
   164          #         "172.20.1.0/24",
   165          #         "172.20.2.0/24"
   166          #     ]
   167          # }'
   168          #
   169          # IPv6 Unicast:
   170          # curl -X POST \
   171          #      -u admin:admin \
   172          #      -H 'Content-Type: application/json' \
   173          #      http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{
   174          #     "attr": {
   175          #         "1": 0,
   176          #         "2": [
   177          #             [
   178          #                 2,
   179          #                 [
   180          #                     65502
   181          #                 ]
   182          #             ]
   183          #         ],
   184          #         "4": 0,
   185          #         "14": {
   186          #             "afi_safi": [
   187          #                 2,
   188          #                 1
   189          #             ],
   190          #             "linklocal_nexthop": "fe80::c002:bff:fe7e:0",
   191          #             "nexthop": "2001:db8::2",
   192          #             "nlri": [
   193          #                 "::2001:db8:2:2/64",
   194          #                 "::2001:db8:2:1/64",
   195          #                 "::2001:db8:2:0/64"
   196          #             ]
   197          #         }
   198          #     }
   199          # }'
   200          if rf == 'ipv4':
   201              return {
   202                  "attr": {
   203                      "3": nexthop,
   204                  },
   205                  "nlri": [prefix],
   206              }
   207          elif rf == 'ipv6':
   208              return {
   209                  "attr": {
   210                      "14": {  # MP_REACH_NLRI
   211                          "afi_safi": [2, 1],
   212                          "nexthop": nexthop,
   213                          "nlri": [prefix],
   214                      },
   215                  },
   216              }
   217          else:
   218              raise ValueError(
   219                  'invalid address family for ipv4/ipv6 unicast: %s' % rf)
   220  
   221      def _construct_ip_unicast_withdraw(self, rf, prefix):
   222          # YABGP v0.4.0
   223          #
   224          # IPv4 Unicast:
   225          # curl -X POST \
   226          #      -u admin:admin \
   227          #      -H 'Content-Type: application/json' \
   228          #      http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{
   229          #     "withdraw": [
   230          #         "172.20.1.0/24",
   231          #         "172.20.2.0/24"
   232          #     ]
   233          # }'
   234          #
   235          # IPv6 Unicast:
   236          # curl -X POST \
   237          #      -u admin:admin \
   238          #      -H 'Content-Type: application/json' \
   239          #      http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{
   240          #     "attr": {
   241          #         "15": {
   242          #             "afi_safi": [
   243          #                 2,
   244          #                 1
   245          #             ],
   246          #             "withdraw": [
   247          #                 "::2001:db8:2:2/64",
   248          #                 "::2001:db8:2:1/64",
   249          #                 "::2001:db8:2:0/64"
   250          #             ]
   251          #         }
   252          #     }
   253          # }'
   254          if rf == 'ipv4':
   255              return {
   256                  "withdraw": [prefix],
   257              }
   258          elif rf == 'ipv6':
   259              return {
   260                  "attr": {
   261                      "15": {  # MP_UNREACH_NLRI
   262                          "afi_safi": [2, 1],
   263                          "withdraw": [prefix],
   264                      },
   265                  },
   266              }
   267          else:
   268              raise ValueError(
   269                  'invalid address family for ipv4/ipv6 unicast: %s' % rf)
   270  
   271      def _construct_flowspec_match(self, matchs):
   272          assert isinstance(matchs, (tuple, list))
   273          ret = {}
   274          for m in matchs:
   275              # m = "source-port '!=2 !=22&!=222'"
   276              # typ = "source-port"
   277              # args = "'!=2 !=22&!=222'"
   278              typ, args = m.split(' ', 1)
   279              # t = 6
   280              t = FLOWSPEC_NAME_TO_TYPE.get(typ, None)
   281              if t is None:
   282                  raise ValueError('invalid flowspec match type: %s' % typ)
   283              # args = "!=2|!=22&!=222"
   284              args = args.strip("'").strip('"').replace(' ', '|')
   285              ret[t] = args
   286          return ret
   287  
   288      def _construct_flowspec_update(self, rf, matchs, thens):
   289          # YABGP v0.4.0
   290          #
   291          # curl -X POST \
   292          #      -u admin:admin \
   293          #      -H 'Content-Type: application/json' \
   294          #      http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{
   295          #     "attr": {
   296          #         "1": 0,
   297          #         "14": {
   298          #             "afi_safi": [
   299          #                 1,
   300          #                 133
   301          #             ],
   302          #             "nexthop": "",
   303          #             "nlri": [
   304          #                 {
   305          #                     "1": "10.0.0.0/24"
   306          #                 }
   307          #             ]
   308          #         },
   309          #         "16": [
   310          #             "traffic-rate:0:0"
   311          #         ],
   312          #         "2": [],
   313          #         "5": 100
   314          #     }
   315          # }'
   316          #
   317          # Format of "thens":
   318          #   "traffic-rate:<AS>:<rate>"
   319          #   "traffic-marking-dscp:<int value>"
   320          #   "redirect-nexthop:<int value>"
   321          #   "redirect-vrf:<RT>"
   322          thens = thens or []
   323          if rf == 'ipv4-flowspec':
   324              afi_safi = [1, 133]
   325          else:
   326              raise ValueError('invalid address family for flowspec: %s' % rf)
   327  
   328          return {
   329              "attr": {
   330                  "14": {  # MP_REACH_NLRI
   331                      "afi_safi": afi_safi,
   332                      "nexthop": "",
   333                      "nlri": [self._construct_flowspec_match(matchs)]
   334                  },
   335                  "16": thens,  # EXTENDED COMMUNITIES
   336              },
   337          }
   338  
   339      def _construct_flowspec_withdraw(self, rf, matchs):
   340          # curl -X POST \
   341          #      -u admin:admin \
   342          #      -H 'Content-Type: application/json' \
   343          #      http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{
   344          #     "attr": {
   345          #         "15": {
   346          #             "afi_safi": [
   347          #                 1,
   348          #                 133
   349          #             ],
   350          #             "withdraw": [
   351          #                 {
   352          #                     "1": "192.88.2.3/24",
   353          #                     "2": "192.89.1.3/24"
   354          #                 },
   355          #                 {
   356          #                     "1": "192.88.4.3/24",
   357          #                     "2": "192.89.2.3/24"
   358          #                 }
   359          #             ]
   360          #         }
   361          #     }
   362          # }'
   363          if rf == 'ipv4-flowspec':
   364              afi_safi = [1, 133]
   365          else:
   366              raise ValueError('invalid address family for flowspec: %s' % rf)
   367  
   368          return {
   369              "attr": {
   370                  "15": {  # MP_UNREACH_NLRI
   371                      "afi_safi": afi_safi,
   372                      "withdraw": [self._construct_flowspec_match(matchs)],
   373                  },
   374              },
   375          }
   376  
   377      def _update_path_attributes(self, path, aspath=None, med=None,
   378                                  local_pref=None):
   379          # ORIGIN: Currently support only IGP(0)
   380          path['attr']['1'] = 0
   381          # AS_PATH: Currently support only AS_SEQUENCE(2)
   382          if aspath is None:
   383              path['attr']['2'] = []
   384          else:
   385              path['attr']['2'] = [[2, aspath]]
   386          # MED
   387          if med is not None:
   388              path['attr']['4'] = med
   389          # LOCAL_PREF
   390          if local_pref is not None:
   391              path['attr']['5'] = local_pref
   392          # TODO:
   393          # Support COMMUNITY and EXTENDED COMMUNITIES
   394  
   395          return path
   396  
   397      def add_route(self, route, rf='ipv4', attribute=None, aspath=None,
   398                    community=None, med=None, extendedcommunity=None,
   399                    nexthop=None, matchs=None, thens=None,
   400                    local_pref=None, identifier=None, reload_config=True):
   401          self.routes.setdefault(route, [])
   402  
   403          for info in list(self.peers.values()):
   404              peer = info['neigh_addr'].split('/')[0]
   405  
   406              if rf in ['ipv4', 'ipv6']:
   407                  nexthop = nexthop or info['local_addr'].split('/')[0]
   408                  path = self._construct_ip_unicast_update(
   409                      rf, route, nexthop)
   410              # TODO:
   411              # Support "evpn" address family
   412              elif rf in ['ipv4-flowspec', 'ipv6-flowspec']:
   413                  path = self._construct_flowspec_update(
   414                      rf, matchs, thens)
   415              else:
   416                  raise ValueError('unsupported address family: %s' % rf)
   417  
   418              self._update_path_attributes(
   419                  path, aspath=aspath, med=med, local_pref=local_pref)
   420  
   421              self._curl_send_update(path, peer)
   422  
   423          self.routes[route].append({
   424              'prefix': route,
   425              'rf': rf,
   426              'attr': attribute,
   427              'next-hop': nexthop,
   428              'as-path': aspath,
   429              'community': community,
   430              'med': med,
   431              'local-pref': local_pref,
   432              'extended-community': extendedcommunity,
   433              'identifier': identifier,
   434              'matchs': matchs,
   435              'thens': thens,
   436          })
   437  
   438      def del_route(self, route, identifier=None, reload_config=True):
   439          new_paths = []
   440          withdraw = None
   441          for p in self.routes.get(route, []):
   442              if p['identifier'] != identifier:
   443                  new_paths.append(p)
   444              else:
   445                  withdraw = p
   446  
   447          if not withdraw:
   448              return
   449          rf = withdraw['rf']
   450  
   451          for info in list(self.peers.values()):
   452              peer = info['neigh_addr'].split('/')[0]
   453  
   454              if rf in ['ipv4', 'ipv6']:
   455                  r = self._construct_ip_unicast_withdraw(rf, route)
   456              elif rf == 'ipv4-flowspec':
   457                  # NOTE: "ipv6-flowspec" does not seem to be supported with
   458                  # YABGP v0.4.0
   459                  matchs = withdraw['matchs']
   460                  r = self._construct_flowspec_withdraw(rf, matchs)
   461              else:
   462                  raise ValueError('unsupported address family: %s' % rf)
   463  
   464              self._curl_send_update(r, peer)
   465  
   466          self.routes[route] = new_paths
   467  
   468      def _get_adj_rib(self, peer, in_out='in'):
   469          peer_addr = self.peer_name(peer)
   470          c = CmdBuffer(' ')
   471          c << "curl -X GET"
   472          c << "-u admin:admin"
   473          c << "-H 'Content-Type: application/json'"
   474          c << "http://localhost:8801/v1-ext/peer/{0}/adj-rib-{1}".format(
   475              peer_addr, in_out)
   476          return json.loads(self.local(str(c), capture=True))
   477  
   478      def get_adj_rib_in(self, peer, rf='ipv4'):
   479          # "rf" should be either of;
   480          #   ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn
   481          # The same as supported "afi_safi" in yabgp.ini
   482          ribs = self._get_adj_rib(peer, 'in')
   483          return ribs.get(rf, {})
   484  
   485      def get_adj_rib_out(self, peer, rf='ipv4'):
   486          # "rf" should be either of;
   487          #   ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn
   488          # The same as supported "afi_safi" in yabgp.ini
   489          ribs = self._get_adj_rib(peer, 'out')
   490          return ribs.get(rf, {})