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