]>
Commit | Line | Data |
---|---|---|
ad9c18f3 CH |
1 | #!/usr/bin/env python |
2 | # -*- coding: utf-8 eval: (blacken-mode 1) -*- | |
3 | # | |
4 | # Copyright (c) 2021, LabN Consulting, L.L.C. | |
5 | # | |
6 | # This program is free software; you can redistribute it and/or | |
7 | # modify it under the terms of the GNU General Public License | |
8 | # as published by the Free Software Foundation; either version 2 | |
9 | # of the License, or (at your option) any later version. | |
10 | # | |
11 | # This program is distributed in the hope that it will be useful, | |
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | # GNU General Public License for more details. | |
15 | # | |
16 | # You should have received a copy of the GNU General Public License along | |
17 | # with this program; see the file COPYING; if not, write to the Free Software | |
18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
19 | # | |
20 | ||
21 | """ | |
22 | test_ospf_clientapi.py: Test the OSPF client API. | |
23 | """ | |
24 | ||
25 | import logging | |
26 | import os | |
27 | import re | |
28 | import signal | |
29 | import subprocess | |
30 | import sys | |
31 | import time | |
32 | from datetime import datetime, timedelta | |
33 | ||
34 | import pytest | |
35 | ||
36 | from lib.common_config import retry, run_frr_cmd, step | |
37 | from lib.micronet import comm_error | |
38 | from lib.topogen import Topogen, TopoRouter | |
39 | from lib.topotest import interface_set_status, json_cmp | |
40 | ||
41 | pytestmark = [pytest.mark.ospfd] | |
42 | ||
43 | CWD = os.path.dirname(os.path.realpath(__file__)) | |
44 | TESTDIR = os.path.abspath(CWD) | |
45 | ||
46 | CLIENTDIR = os.path.abspath(os.path.join(CWD, "../../../ospfclient")) | |
47 | if not os.path.exists(CLIENTDIR): | |
48 | CLIENTDIR = os.path.join(CWD, "/usr/lib/frr") | |
49 | ||
50 | assert os.path.exists( | |
51 | os.path.join(CLIENTDIR, "ospfclient.py") | |
52 | ), "can't locate ospfclient.py" | |
53 | ||
54 | ||
55 | # ---------- | |
56 | # Test Setup | |
57 | # ---------- | |
58 | ||
59 | ||
60 | @pytest.fixture(scope="function", name="tgen") | |
61 | def _tgen(request): | |
62 | "Setup/Teardown the environment and provide tgen argument to tests" | |
63 | nrouters = request.param | |
64 | topodef = {f"sw{i}": (f"r{i}", f"r{i+1}") for i in range(1, nrouters)} | |
65 | ||
66 | tgen = Topogen(topodef, request.module.__name__) | |
67 | tgen.start_topology() | |
68 | ||
69 | router_list = tgen.routers() | |
70 | for _, router in router_list.items(): | |
71 | router.load_config(TopoRouter.RD_ZEBRA, "zebra.conf") | |
72 | router.load_config(TopoRouter.RD_OSPF, "ospfd.conf") | |
73 | router.net.daemons_options["ospfd"] = "--apiserver" | |
74 | ||
75 | tgen.start_router() | |
76 | ||
77 | yield tgen | |
78 | ||
79 | tgen.stop_topology() | |
80 | ||
81 | ||
82 | # Fixture that executes before each test | |
83 | @pytest.fixture(autouse=True) | |
84 | def skip_on_failure(tgen): | |
85 | if tgen.routers_have_failure(): | |
86 | pytest.skip("skipped because of previous test failure") | |
87 | ||
88 | ||
89 | # ------------ | |
90 | # Test Utility | |
91 | # ------------ | |
92 | ||
93 | ||
94 | @retry(retry_timeout=45) | |
95 | def verify_ospf_database(tgen, dut, input_dict, cmd="show ip ospf database json"): | |
96 | del tgen | |
97 | show_ospf_json = run_frr_cmd(dut, cmd, isjson=True) | |
98 | if not bool(show_ospf_json): | |
99 | return "ospf is not running" | |
100 | result = json_cmp(show_ospf_json, input_dict) | |
101 | return str(result) if result else None | |
102 | ||
103 | ||
104 | def myreadline(f): | |
105 | buf = b"" | |
106 | while True: | |
107 | # logging.info("READING 1 CHAR") | |
108 | c = f.read(1) | |
109 | if not c: | |
110 | return buf if buf else None | |
111 | buf += c | |
112 | # logging.info("READ CHAR: '%s'", c) | |
113 | if c == b"\n": | |
114 | return buf | |
115 | ||
116 | ||
117 | def _wait_output(p, regex, timeout=120): | |
118 | retry_until = datetime.now() + timedelta(seconds=timeout) | |
119 | while datetime.now() < retry_until: | |
120 | # line = p.stdout.readline() | |
121 | line = myreadline(p.stdout) | |
122 | if not line: | |
123 | assert None, "Timeout waiting for '{}'".format(regex) | |
124 | line = line.decode("utf-8") | |
125 | line = line.rstrip() | |
126 | if line: | |
127 | logging.debug("GOT LINE: '%s'", line) | |
128 | m = re.search(regex, line) | |
129 | if m: | |
130 | return m | |
131 | assert None, "Failed to get output withint {}s".format(timeout) | |
132 | ||
133 | ||
134 | # ----- | |
135 | # Tests | |
136 | # ----- | |
137 | ||
138 | ||
139 | def _test_reachability(tgen, testbin): | |
140 | waitlist = [ | |
141 | "192.168.0.1,192.168.0.2,192.168.0.4", | |
142 | "192.168.0.2,192.168.0.4", | |
143 | "192.168.0.1,192.168.0.2,192.168.0.4", | |
144 | ] | |
145 | r2 = tgen.gears["r2"] | |
146 | r3 = tgen.gears["r3"] | |
147 | ||
148 | wait_args = [f"--wait={x}" for x in waitlist] | |
149 | ||
150 | p = None | |
151 | try: | |
152 | step("reachable: check for initial reachability") | |
153 | p = r3.popen( | |
154 | ["/usr/bin/timeout", "120", testbin, "-v", *wait_args], | |
155 | encoding=None, # don't buffer | |
156 | stdin=subprocess.DEVNULL, | |
157 | stdout=subprocess.PIPE, | |
158 | stderr=subprocess.STDOUT, | |
159 | ) | |
160 | _wait_output(p, "SUCCESS: {}".format(waitlist[0])) | |
161 | ||
162 | step("reachable: check for modified reachability") | |
163 | interface_set_status(r2, "r2-eth0", False) | |
164 | _wait_output(p, "SUCCESS: {}".format(waitlist[1])) | |
165 | ||
166 | step("reachable: check for restored reachability") | |
167 | interface_set_status(r2, "r2-eth0", True) | |
168 | _wait_output(p, "SUCCESS: {}".format(waitlist[2])) | |
169 | except Exception as error: | |
170 | logging.error("ERROR: %s", error) | |
171 | raise | |
172 | finally: | |
173 | if p: | |
174 | p.terminate() | |
175 | p.wait() | |
176 | ||
177 | ||
178 | @pytest.mark.parametrize("tgen", [4], indirect=True) | |
179 | def test_ospf_reachability(tgen): | |
180 | testbin = os.path.join(TESTDIR, "ctester.py") | |
181 | rc, o, e = tgen.gears["r2"].net.cmd_status([testbin, "--help"]) | |
182 | logging.info("%s --help: rc: %s stdout: '%s' stderr: '%s'", testbin, rc, o, e) | |
183 | _test_reachability(tgen, testbin) | |
184 | ||
185 | ||
186 | def _test_add_data(tgen, apibin): | |
187 | "Test adding opaque data to domain" | |
188 | ||
189 | r1 = tgen.gears["r1"] | |
190 | ||
191 | step("add opaque: add opaque link local") | |
192 | ||
193 | p = None | |
194 | try: | |
195 | p = r1.popen([apibin, "-v", "add,9,10.0.1.1,230,2,00000202"]) | |
196 | input_dict = { | |
197 | "routerId": "192.168.0.1", | |
198 | "areas": { | |
199 | "1.2.3.4": { | |
200 | "linkLocalOpaqueLsa": [ | |
201 | { | |
202 | "lsId": "230.0.0.2", | |
203 | "advertisedRouter": "192.168.0.1", | |
204 | "sequenceNumber": "80000001", | |
205 | } | |
206 | ], | |
207 | } | |
208 | }, | |
209 | } | |
210 | # Wait for it to show up | |
211 | assert verify_ospf_database(tgen, r1, input_dict) is None | |
212 | ||
213 | input_dict = { | |
214 | "linkLocalOpaqueLsa": { | |
215 | "areas": { | |
216 | "1.2.3.4": [ | |
217 | { | |
218 | "linkStateId": "230.0.0.2", | |
219 | "advertisingRouter": "192.168.0.1", | |
220 | "lsaSeqNumber": "80000001", | |
221 | "opaqueData": "00000202", | |
222 | }, | |
223 | ], | |
224 | } | |
225 | }, | |
226 | } | |
227 | # verify content | |
228 | json_cmd = "show ip ospf da opaque-link json" | |
229 | assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None | |
230 | ||
231 | step("reset client, add opaque area, verify link local flushing") | |
232 | ||
233 | p.send_signal(signal.SIGINT) | |
234 | time.sleep(2) | |
235 | p.wait() | |
236 | p = None | |
237 | p = r1.popen([apibin, "-v", "add,10,1.2.3.4,231,1,00010101"]) | |
238 | input_dict = { | |
239 | "routerId": "192.168.0.1", | |
240 | "areas": { | |
241 | "1.2.3.4": { | |
242 | "linkLocalOpaqueLsa": [ | |
243 | { | |
244 | "lsId": "230.0.0.2", | |
245 | "advertisedRouter": "192.168.0.1", | |
246 | "sequenceNumber": "80000001", | |
247 | "lsaAge": 3600, | |
248 | } | |
249 | ], | |
250 | "areaLocalOpaqueLsa": [ | |
251 | { | |
252 | "lsId": "231.0.0.1", | |
253 | "advertisedRouter": "192.168.0.1", | |
254 | "sequenceNumber": "80000001", | |
255 | }, | |
256 | ], | |
257 | } | |
258 | }, | |
259 | } | |
260 | # Wait for it to show up | |
261 | assert verify_ospf_database(tgen, r1, input_dict) is None | |
262 | ||
263 | input_dict = { | |
264 | "areaLocalOpaqueLsa": { | |
265 | "areas": { | |
266 | "1.2.3.4": [ | |
267 | { | |
268 | "linkStateId": "231.0.0.1", | |
269 | "advertisingRouter": "192.168.0.1", | |
270 | "lsaSeqNumber": "80000001", | |
271 | "opaqueData": "00010101", | |
272 | }, | |
273 | ], | |
274 | } | |
275 | }, | |
276 | } | |
277 | # verify content | |
278 | json_cmd = "show ip ospf da opaque-area json" | |
279 | assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None | |
280 | ||
281 | step("reset client, add opaque AS, verify area flushing") | |
282 | ||
283 | p.send_signal(signal.SIGINT) | |
284 | time.sleep(2) | |
285 | p.wait() | |
286 | p = None | |
287 | ||
288 | p = r1.popen([apibin, "-v", "add,11,232,3,deadbeaf01234567"]) | |
289 | input_dict = { | |
290 | "routerId": "192.168.0.1", | |
291 | "areas": { | |
292 | "1.2.3.4": { | |
293 | "areaLocalOpaqueLsa": [ | |
294 | { | |
295 | "lsId": "231.0.0.1", | |
296 | "advertisedRouter": "192.168.0.1", | |
297 | "sequenceNumber": "80000001", | |
298 | "lsaAge": 3600, | |
299 | }, | |
300 | ], | |
301 | } | |
302 | }, | |
303 | "asExternalOpaqueLsa": [ | |
304 | { | |
305 | "lsId": "232.0.0.3", | |
306 | "advertisedRouter": "192.168.0.1", | |
307 | "sequenceNumber": "80000001", | |
308 | }, | |
309 | ], | |
310 | } | |
311 | # Wait for it to show up | |
312 | assert verify_ospf_database(tgen, r1, input_dict) is None | |
313 | ||
314 | input_dict = { | |
315 | "asExternalOpaqueLsa": [ | |
316 | { | |
317 | "linkStateId": "232.0.0.3", | |
318 | "advertisingRouter": "192.168.0.1", | |
319 | "lsaSeqNumber": "80000001", | |
320 | "opaqueData": "deadbeaf01234567", | |
321 | }, | |
322 | ] | |
323 | } | |
324 | # verify content | |
325 | json_cmd = "show ip ospf da opaque-as json" | |
326 | assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None | |
327 | ||
328 | step("stop client, verify AS flushing") | |
329 | ||
330 | p.send_signal(signal.SIGINT) | |
331 | time.sleep(2) | |
332 | p.wait() | |
333 | p = None | |
334 | ||
335 | input_dict = { | |
336 | "routerId": "192.168.0.1", | |
337 | "asExternalOpaqueLsa": [ | |
338 | { | |
339 | "lsId": "232.0.0.3", | |
340 | "advertisedRouter": "192.168.0.1", | |
341 | "sequenceNumber": "80000001", | |
342 | "lsaAge": 3600, | |
343 | }, | |
344 | ], | |
345 | } | |
346 | # Wait for it to be flushed | |
347 | assert verify_ospf_database(tgen, r1, input_dict) is None | |
348 | ||
349 | step("start client adding opaque domain, verify new sequence number and data") | |
350 | ||
351 | # Originate it again | |
352 | p = r1.popen([apibin, "-v", "add,11,232,3,ebadf00d"]) | |
353 | input_dict = { | |
354 | "routerId": "192.168.0.1", | |
355 | "asExternalOpaqueLsa": [ | |
356 | { | |
357 | "lsId": "232.0.0.3", | |
358 | "advertisedRouter": "192.168.0.1", | |
359 | "sequenceNumber": "80000002", | |
360 | }, | |
361 | ], | |
362 | } | |
363 | assert verify_ospf_database(tgen, r1, input_dict) is None | |
364 | ||
365 | input_dict = { | |
366 | "asExternalOpaqueLsa": [ | |
367 | { | |
368 | "linkStateId": "232.0.0.3", | |
369 | "advertisingRouter": "192.168.0.1", | |
370 | "lsaSeqNumber": "80000002", | |
371 | "opaqueData": "ebadf00d", | |
372 | }, | |
373 | ] | |
374 | } | |
375 | # verify content | |
376 | json_cmd = "show ip ospf da opaque-as json" | |
377 | assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None | |
378 | ||
379 | p.send_signal(signal.SIGINT) | |
380 | time.sleep(2) | |
381 | p.wait() | |
382 | p = None | |
383 | ||
384 | except Exception: | |
385 | if p: | |
386 | p.terminate() | |
387 | if p.wait(): | |
388 | comm_error(p) | |
389 | p = None | |
390 | raise | |
391 | finally: | |
392 | if p: | |
393 | p.terminate() | |
394 | p.wait() | |
395 | ||
396 | ||
397 | @pytest.mark.parametrize("tgen", [2], indirect=True) | |
398 | def test_ospf_opaque_add_data3(tgen): | |
399 | apibin = os.path.join(CLIENTDIR, "ospfclient.py") | |
400 | rc, o, e = tgen.gears["r2"].net.cmd_status([apibin, "--help"]) | |
401 | logging.info("%s --help: rc: %s stdout: '%s' stderr: '%s'", apibin, rc, o, e) | |
402 | _test_add_data(tgen, apibin) | |
403 | ||
404 | ||
405 | def _test_opaque_add_del(tgen, apibin): | |
406 | "Test adding opaque data to domain" | |
407 | ||
408 | r1 = tgen.gears["r1"] | |
409 | r2 = tgen.gears["r2"] | |
410 | ||
411 | p = None | |
412 | pread = None | |
413 | try: | |
414 | step("reachable: check for add notification") | |
415 | pread = r2.popen( | |
416 | ["/usr/bin/timeout", "120", apibin, "-v"], | |
417 | encoding=None, # don't buffer | |
418 | stdin=subprocess.DEVNULL, | |
419 | stdout=subprocess.PIPE, | |
420 | stderr=subprocess.STDOUT, | |
421 | ) | |
422 | p = r1.popen([apibin, "-v", "add,11,232,3,ebadf00d"]) | |
423 | ||
424 | # Wait for add notification | |
425 | # RECV: LSA update msg for LSA 232.0.0.3 in area 0.0.0.0 seq 0x80000001 len 24 age 9 | |
426 | ||
427 | ls_id = "232.0.0.3" | |
428 | waitfor = "RECV:.*update msg.*LSA {}.*age ([0-9]+)".format(ls_id) | |
429 | _ = _wait_output(pread, waitfor) | |
430 | ||
431 | p.terminate() | |
432 | if p.wait(): | |
433 | comm_error(p) | |
434 | ||
435 | # step("reachable: check for flush/age out") | |
436 | # # Wait for max age notification | |
437 | # waitfor = "RECV:.*update msg.*LSA {}.*age 3600".format(ls_id) | |
438 | # _wait_output(pread, waitfor) | |
439 | ||
440 | step("reachable: check for delete") | |
441 | # Wait for delete notification | |
442 | waitfor = "RECV:.*delete msg.*LSA {}.*".format(ls_id) | |
443 | _wait_output(pread, waitfor) | |
444 | except Exception: | |
445 | if p: | |
446 | p.terminate() | |
447 | if p.wait(): | |
448 | comm_error(p) | |
449 | p = None | |
450 | raise | |
451 | finally: | |
452 | if pread: | |
453 | pread.terminate() | |
454 | pread.wait() | |
455 | if p: | |
456 | p.terminate() | |
457 | p.wait() | |
458 | ||
459 | ||
460 | @pytest.mark.parametrize("tgen", [2], indirect=True) | |
461 | def test_ospf_opaque_delete_data3(tgen): | |
462 | apibin = os.path.join(CLIENTDIR, "ospfclient.py") | |
463 | rc, o, e = tgen.gears["r2"].net.cmd_status([apibin, "--help"]) | |
464 | logging.info("%s --help: rc: %s stdout: '%s' stderr: '%s'", apibin, rc, o, e) | |
465 | _test_opaque_add_del(tgen, apibin) | |
466 | ||
467 | ||
468 | if __name__ == "__main__": | |
469 | args = ["-s"] + sys.argv[1:] | |
470 | sys.exit(pytest.main(args)) |