1# Copyright (C) 2017-2021 Pier Carlo Chiodi 2# 3# This program is free software: you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation, either version 3 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16import six 17 18from pierky.arouteserver.tests.live_tests.base import LiveScenario 19from pierky.arouteserver.tests.live_tests.bird import BIRDInstanceIPv4, \ 20 BIRDInstanceIPv6 21from pierky.arouteserver.tests.live_tests.openbgpd import OpenBGPD60Instance, \ 22 OpenBGPD61Instance, \ 23 OpenBGPD62Instance, \ 24 OpenBGPD63Instance, \ 25 OpenBGPD64Instance 26 27# ----------------------------------------------------------------------- 28# FULL DOCUMENTATION ON 29# 30# https://arouteserver.readthedocs.io/en/latest/LIVETESTS.html#how-to-build-custom-scenarios 31# ----------------------------------------------------------------------- 32 33class SkeletonScenario(LiveScenario): 34 """The base class that describes your scenario. 35 36 A multi level structure of parent/child classes allows to decuple 37 test functions (that is, the scenario expectations) from the BGP 38 speakers configuration and IP addresses and prefixes that are used 39 to build the scenario. 40 41 The example structure looks like the following: 42 43 - base.py: a base class (this one) where test functions are implemented 44 in an IP version independent way; 45 46 - test_XXX.py: BGP speaker specific and IP version specific 47 classes where the real IP addresses and prefixes are provided 48 in a dictionary made of ``prefix_ID: real_IP_prefix`` entries. 49 50 If it's needed by the scenario, the derived classes must also fill the 51 ``AS_SET`` and ``R_SET`` dictionaries with the expected content of any 52 expanded AS-SETs used in IRRDB validation: 53 54 - ``AS_SET``'s items must be in the format 55 ``<AS_SET_name>: <list_of_authorized_origin_ASNs>``. 56 57 - ``R_SET``'s items must be in the format 58 ``<AS_SET_name>: <list_of_authorized_prefix_IDs>`` (where prefix 59 IDs are those reported in the ``DATA`` dictionary). 60 61 Example:: 62 63 AS_SET = { 64 "AS-AS1": [1], 65 "AS-AS1_CUSTOMERS": [101], 66 "AS-AS2": [2], 67 "AS-AS2_CUSTOMERS": [101] 68 } 69 R_SET = { 70 "AS-AS1": [ 71 "AS1_allowed_prefixes" 72 ], 73 "AS-AS1_CUSTOMERS": [ 74 "AS101_prefixes" 75 ] 76 } 77 78 Finally, this class must implement all the tests that are shared between 79 the IPv4 and the IPv6 version of this scenario. 80 81 Writing test functions 82 ---------------------- 83 84 Test functions names must start with "test_"; tests are processed in 85 alphabetical order; each test is independent from the others. 86 87 Some helper functions can be used to define expectations. 88 89 - ``self.session_is_up()``: test if a BGP session between the two 90 instances is up. 91 92 Details here (URL wraps): 93 94 http://arouteserver.readthedocs.io/en/latest/LIVETESTS_CODEDOC.html# 95 pierky.arouteserver.tests.live_tests.base.LiveScenario.session_is_up 96 97 - ``self.receive_route()``: test if the BGP speaker receives the expected 98 route(s). 99 100 Details here (URL wraps): 101 102 http://arouteserver.readthedocs.io/en/latest/LIVETESTS_CODEDOC.html# 103 pierky.arouteserver.tests.live_tests.base.LiveScenario.receive_route 104 105 106 - ``self.log_contains()``: test if the BGP speaker's log contains the 107 expected message. 108 109 Details here (URL wraps): 110 111 http://arouteserver.readthedocs.io/en/latest/LIVETESTS_CODEDOC.html# 112 pierky.arouteserver.tests.live_tests.base.LiveScenario.log_contains 113 """ 114 115 # Leave this to False to avoid nose to use this abstract class to run 116 # tests. Only derived, more specific classes (test_XXX.py) must have 117 # this set to True. 118 __test__ = False 119 120 # This allows to use files and directories paths which are relative 121 # to this scenario root directory. 122 MODULE_PATH = __file__ 123 124 # The following attributes must be setted in derived classes. 125 CONFIG_BUILDER_CLASS = None 126 RS_INSTANCE_CLASS = None 127 CLIENT_INSTANCE_CLASS = None 128 IP_VER = None 129 130 # If needed for IRRDB validation, fill this dictionary with pairs 131 # in the format "<AS_SET_name>": [<list_of_authorized_origin_ASNs>]. 132 # See the example in the class docstring above. 133 AS_SET = { 134 } 135 136 # If needed for IRRDB validation, fill this dictionary with pairs 137 # in the format "<AS_SET_name>": [<list_of_authorized_prefix_IDs>]. 138 # See the example in the class docstring above. 139 R_SET = { 140 } 141 142 @classmethod 143 def _setup_instances(cls): 144 """Declare the BGP speaker instances that are used in this scenario. 145 146 The ``cls.INSTANCES`` attribute is a list of all the instances that 147 are used in this scenario. It is used to render local Jinja2 templates 148 and to transform them into real BGP speaker configuration files. 149 150 The ``cls.RS_INSTANCE_CLASS`` and ``cls.CLIENT_INSTANCE_CLASS`` 151 attributes are set by the derived classes (test_XXX.py) and 152 represent the route server class and the other BGP speakers class 153 respectively. 154 155 - The first argument is the instance name. 156 157 - The second argument is the IP address that is used to run the 158 instance. Here, the ``cls.DATA`` dictionary is used to lookup the 159 real IP address to use, which is configured in the derived classes 160 (test_XXX.py). 161 162 - The third argument is a list of files that are mounted from the local 163 host (where Docker is running) to the container (the BGP speaker). 164 The list is made of pairs in the form 165 ``(local_file, container_file)``. 166 The ``cls.build_rs_cfg`` and ``cls.build_other_cfg`` helper functions 167 allow to render Jinja2 templates and to obtain the path of the local 168 output files. 169 170 For the route server, the configuration is built using ARouteServer's 171 library on the basis of the options given in the YAML files. 172 173 For the other BGP speakers, the configuration must be provided in the 174 Jinja2 files within the scenario directory. 175 """ 176 177 cls.INSTANCES = [ 178 cls._setup_rs_instance(), 179 180 cls.CLIENT_INSTANCE_CLASS( 181 "AS1", 182 cls.DATA["AS1_IPAddress"], 183 [ 184 ( 185 cls.build_other_cfg("AS1.j2"), 186 "/etc/bird/bird.conf" 187 ) 188 ] 189 ), 190 cls.CLIENT_INSTANCE_CLASS( 191 "AS2", 192 cls.DATA["AS2_IPAddress"], 193 [ 194 ( 195 cls.build_other_cfg("AS2.j2"), 196 "/etc/bird/bird.conf" 197 ) 198 ] 199 ) 200 ] 201 202 @classmethod 203 def _setup_rs_instance(cls): 204 if cls.RS_INSTANCE_CLASS is OpenBGPD60Instance: 205 return cls.RS_INSTANCE_CLASS( 206 "rs", 207 cls.DATA["rs_IPAddress"], 208 [ 209 ( 210 cls.build_rs_cfg("openbgpd", "main.j2", "rs.conf", None, 211 target_version="6.0"), 212 "/etc/bgpd.conf" 213 ) 214 ] 215 ) 216 if cls.RS_INSTANCE_CLASS is OpenBGPD61Instance: 217 return cls.RS_INSTANCE_CLASS( 218 "rs", 219 cls.DATA["rs_IPAddress"], 220 [ 221 ( 222 cls.build_rs_cfg("openbgpd", "main.j2", "rs.conf", None, 223 target_version="6.1"), 224 "/etc/bgpd.conf" 225 ) 226 ] 227 ) 228 if cls.RS_INSTANCE_CLASS is OpenBGPD62Instance: 229 return cls.RS_INSTANCE_CLASS( 230 "rs", 231 cls.DATA["rs_IPAddress"], 232 [ 233 ( 234 cls.build_rs_cfg("openbgpd", "main.j2", "rs.conf", None, 235 target_version="6.2"), 236 "/etc/bgpd.conf" 237 ) 238 ] 239 ) 240 if cls.RS_INSTANCE_CLASS is OpenBGPD63Instance: 241 return cls.RS_INSTANCE_CLASS( 242 "rs", 243 cls.DATA["rs_IPAddress"], 244 [ 245 ( 246 cls.build_rs_cfg("openbgpd", "main.j2", "rs.conf", None, 247 target_version="6.3"), 248 "/etc/bgpd.conf" 249 ) 250 ] 251 ) 252 if cls.RS_INSTANCE_CLASS is OpenBGPD64Instance: 253 return cls.RS_INSTANCE_CLASS( 254 "rs", 255 cls.DATA["rs_IPAddress"], 256 [ 257 ( 258 cls.build_rs_cfg("openbgpd", "main.j2", "rs.conf", None, 259 target_version="6.4"), 260 "/etc/bgpd.conf" 261 ) 262 ] 263 ) 264 if cls.RS_INSTANCE_CLASS is BIRDInstanceIPv4 or \ 265 cls.RS_INSTANCE_CLASS is BIRDInstanceIPv6: 266 return cls.RS_INSTANCE_CLASS( 267 "rs", 268 cls.DATA["rs_IPAddress"], 269 [ 270 ( 271 cls.build_rs_cfg("bird", "main.j2", "rs.conf", cls.IP_VER), 272 "/etc/bird/bird.conf" 273 ) 274 ] 275 ) 276 raise NotImplementedError("RS_INSTANCE_CLASS unknown: {}".format( 277 cls.RS_INSTANCE_CLASS.__name__)) 278 279 def set_instance_variables(self): 280 """Simply set local attributes for an easier usage later 281 282 The argument of ``self._get_instance_by_name()`` must be one of 283 the instance names used in ``_setup_instances()``. 284 """ 285 self.AS1 = self._get_instance_by_name("AS1") 286 self.AS2 = self._get_instance_by_name("AS2") 287 self.rs = self._get_instance_by_name("rs") 288 289 def test_010_setup(self): 290 """{}: instances setup""" 291 pass 292 293 def test_020_sessions_up(self): 294 """{}: sessions are up""" 295 self.session_is_up(self.rs, self.AS1) 296 self.session_is_up(self.rs, self.AS2) 297 298 def test_030_rs_receives_AS2_prefix(self): 299 """{}: rs receives AS2 prefix""" 300 self.receive_route(self.rs, self.DATA["AS2_prefix1"], 301 other_inst=self.AS2, as_path="2") 302 303 def test_030_rs_rejects_bogon(self): 304 """{}: rs rejects bogon prefix""" 305 self.log_contains(self.rs, 306 "prefix is bogon - REJECTING {}".format( 307 self.DATA["AS2_bogon1"])) 308 self.receive_route(self.rs, self.DATA["AS2_bogon1"], 309 other_inst=self.AS2, as_path="2", 310 filtered=True) 311 # AS1 should not receive the bogon prefix from the route server 312 with six.assertRaisesRegex(self, AssertionError, "Routes not found"): 313 self.receive_route(self.AS1, self.DATA["AS2_bogon1"]) 314 315 def test_030_custom_test(self): 316 """{}: custom test""" 317