]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/ospfapi/test_ospf_clientapi.py
ospfclient: add router id support to python client
[mirror_frr.git] / tests / topotests / ospfapi / test_ospf_clientapi.py
CommitLineData
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"""
22test_ospf_clientapi.py: Test the OSPF client API.
23"""
24
25import logging
26import os
27import re
28import signal
29import subprocess
30import sys
31import time
32from datetime import datetime, timedelta
33
34import pytest
35
36from lib.common_config import retry, run_frr_cmd, step
37from lib.micronet import comm_error
38from lib.topogen import Topogen, TopoRouter
39from lib.topotest import interface_set_status, json_cmp
40
41pytestmark = [pytest.mark.ospfd]
42
43CWD = os.path.dirname(os.path.realpath(__file__))
44TESTDIR = os.path.abspath(CWD)
45
46CLIENTDIR = os.path.abspath(os.path.join(CWD, "../../../ospfclient"))
47if not os.path.exists(CLIENTDIR):
48 CLIENTDIR = os.path.join(CWD, "/usr/lib/frr")
49
50assert 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")
61def _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)
84def 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)
95def 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
104def 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
117def _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
139def _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)
179def 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
186def _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)
398def 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
405def _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)
461def 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
468if __name__ == "__main__":
469 args = ["-s"] + sys.argv[1:]
470 sys.exit(pytest.main(args))