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