diff options
Diffstat (limited to 'ospfclient/ospfclient.py')
| -rwxr-xr-x | ospfclient/ospfclient.py | 208 |
1 files changed, 117 insertions, 91 deletions
diff --git a/ospfclient/ospfclient.py b/ospfclient/ospfclient.py index 8e3c68445f..7477ef8191 100755 --- a/ospfclient/ospfclient.py +++ b/ospfclient/ospfclient.py @@ -1,24 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 eval: (blacken-mode 1) -*- +# SPDX-License-Identifier: GPL-2.0-or-later # # December 22 2021, Christian Hopps <chopps@labn.net> # # Copyright 2021-2022, LabN Consulting, L.L.C. # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; see the file COPYING; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -# import argparse import asyncio @@ -255,6 +242,16 @@ def nsm_name(state): return names.get(state, str(state)) +class WithNothing: + "An object that does nothing when used with `with` statement." + + async def __aenter__(self): + return + + async def __aexit__(self, *args, **kwargs): + return + + # -------------- # Client Classes # -------------- @@ -560,15 +557,17 @@ class OspfOpaqueClient(OspfApiClient): Args: server: hostname or IP address of server default is "localhost" + wait_ready: if True then wait for OSPF to signal ready, in newer versions + FRR ospfd is always ready so this overhead can be skipped. + default is False. Raises: Will raise exceptions for failures with various `socket` modules functions such as `socket.socket`, `socket.setsockopt`, `socket.bind`. """ - def __init__(self, server="localhost"): + def __init__(self, server="localhost", wait_ready=False): handlers = { - MSG_READY_NOTIFY: self._ready_msg, MSG_LSA_UPDATE_NOTIFY: self._lsa_change_msg, MSG_LSA_DELETE_NOTIFY: self._lsa_change_msg, MSG_NEW_IF: self._if_msg, @@ -578,9 +577,13 @@ class OspfOpaqueClient(OspfApiClient): MSG_REACHABLE_CHANGE: self._reachable_msg, MSG_ROUTER_ID_CHANGE: self._router_id_msg, } + if wait_ready: + handlers[MSG_READY_NOTIFY] = self._ready_msg + super().__init__(server, handlers) - self.ready_lock = Lock() + self.wait_ready = wait_ready + self.ready_lock = Lock() if wait_ready else WithNothing() self.ready_cond = { LSA_TYPE_OPAQUE_LINK: {}, LSA_TYPE_OPAQUE_AREA: {}, @@ -617,13 +620,9 @@ class OspfOpaqueClient(OspfApiClient): mp = struct.pack(msg_fmt[mt], lsa_type, otype) await self.msg_send_raises(mt, mp) - async def _assure_opaque_ready(self, lsa_type, otype): - async with self.ready_lock: - if self.ready_cond[lsa_type].get(otype) is True: - return - - await self._register_opaque_data(lsa_type, otype) - await self.wait_opaque_ready(lsa_type, otype) + # If we are not waiting, mark ready for register check + if not self.wait_ready: + self.ready_cond[lsa_type][otype] = True async def _handle_msg_loop(self): try: @@ -656,6 +655,8 @@ class OspfOpaqueClient(OspfApiClient): return lsa async def _ready_msg(self, mt, msg, extra, lsa_type, otype, addr): + assert self.wait_ready + if lsa_type == LSA_TYPE_OPAQUE_LINK: e = "ifaddr {}".format(ip(addr)) elif lsa_type == LSA_TYPE_OPAQUE_AREA: @@ -825,6 +826,7 @@ class OspfOpaqueClient(OspfApiClient): Raises: See `msg_send_raises` """ + assert self.ready_cond.get(lsa_type, {}).get(otype) is True, "Not Registered!" if lsa_type == LSA_TYPE_OPAQUE_LINK: ifaddr, aid = int(addr), 0 @@ -842,7 +844,6 @@ class OspfOpaqueClient(OspfApiClient): *OspfOpaqueClient._opaque_args(lsa_type, otype, oid, data), ) msg += data - await self._assure_opaque_ready(lsa_type, otype) await self.msg_send_raises(mt, msg) async def delete_opaque_data(self, addr, lsa_type, otype, oid, flags=0): @@ -854,21 +855,31 @@ class OspfOpaqueClient(OspfApiClient): Args: addr: depends on lsa_type, LINK => ifaddr, AREA => area ID, AS => ignored lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} - otype: (octet) opaque type. Note: the type will be registered if the user - has not explicity done that yet with `register_opaque_data`. + otype: (octet) opaque type. oid: (3 octets) ID of this opaque data flags: (octet) optional flags (e.g., OSPF_API_DEL_ZERO_LEN_LSA, defaults to no flags) Raises: See `msg_send_raises` """ - if (lsa_type, otype) in self.opaque_change_cb: - del self.opaque_change_cb[(lsa_type, otype)] + assert self.ready_cond.get(lsa_type, {}).get(otype) is True, "Not Registered!" mt = MSG_DELETE_REQUEST - await self._assure_opaque_ready(lsa_type, otype) mp = struct.pack(msg_fmt[mt], int(addr), lsa_type, otype, flags, oid) await self.msg_send_raises(mt, mp) + async def is_registered(self, lsa_type, otype): + """Determine if an (lsa_type, otype) tuple has been registered with FRR + + This determines if the type has been registered, but not necessarily if it is + ready, if that is required use the `wait_opaque_ready` metheod. + + Args: + lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} + otype: (octet) opaque type. + """ + async with self.ready_lock: + return self.ready_cond.get(lsa_type, {}).get(otype) is not None + async def register_opaque_data(self, lsa_type, otype, callback=None): """Register intent to advertise opaque data. @@ -878,8 +889,7 @@ class OspfOpaqueClient(OspfApiClient): Args: lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} - otype: (octet) opaque type. Note: the type will be registered if the user - has not explicity done that yet with `register_opaque_data`. + otype: (octet) opaque type. callback: if given, callback will be called when changes are received for LSA of the given (lsa_type, otype). The callbacks signature is: @@ -895,6 +905,10 @@ class OspfOpaqueClient(OspfApiClient): Raises: See `msg_send_raises` """ + assert not await self.is_registered( + lsa_type, otype + ), "Registering registered type" + if callback: self.opaque_change_cb[(lsa_type, otype)] = callback elif (lsa_type, otype) in self.opaque_change_cb: @@ -913,6 +927,8 @@ class OspfOpaqueClient(OspfApiClient): if cond is True: return + assert self.wait_ready + logging.debug( "waiting for ready %s opaque-type %s", lsa_typename(lsa_type), otype ) @@ -933,8 +949,7 @@ class OspfOpaqueClient(OspfApiClient): Args: lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} - otype: (octet) opaque type. Note: the type will be registered if the user - has not explicity done that yet with `register_opaque_data`. + otype: (octet) opaque type. callback: if given, callback will be called when changes are received for LSA of the given (lsa_type, otype). The callbacks signature is: @@ -951,17 +966,8 @@ class OspfOpaqueClient(OspfApiClient): See `msg_send_raises` """ - if callback: - self.opaque_change_cb[(lsa_type, otype)] = callback - elif (lsa_type, otype) in self.opaque_change_cb: - logging.warning( - "OSPFCLIENT: register: removing callback for %s opaque-type %s", - lsa_typename(lsa_type), - otype, - ) - del self.opaque_change_cb[(lsa_type, otype)] - - return await self._assure_opaque_ready(lsa_type, otype) + await self.register_opaque_data(lsa_type, otype, callback) + await self.wait_opaque_ready(lsa_type, otype) async def unregister_opaque_data(self, lsa_type, otype): """Unregister intent to advertise opaque data. @@ -971,11 +977,13 @@ class OspfOpaqueClient(OspfApiClient): Args: lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} - otype: (octet) opaque type. Note: the type will be registered if the user - has not explicity done that yet with `register_opaque_data`. + otype: (octet) opaque type. Raises: See `msg_send_raises` """ + assert await self.is_registered( + lsa_type, otype + ), "Unregistering unregistered type" if (lsa_type, otype) in self.opaque_change_cb: del self.opaque_change_cb[(lsa_type, otype)] @@ -1081,6 +1089,17 @@ class OspfOpaqueClient(OspfApiClient): # ================ # CLI/Script Usage # ================ +def next_action(action_list=None): + "Get next action from list or STDIN" + if action_list: + for action in action_list: + yield action + else: + while True: + action = input("") + if not action: + break + yield action.strip() async def async_main(args): @@ -1099,50 +1118,53 @@ async def async_main(args): await c.req_ism_states() await c.req_nsm_states() - if args.actions: - for action in args.actions: - _s = action.split(",") - what = _s.pop(False) - if what.casefold() == "wait": - stime = int(_s.pop(False)) - logging.info("waiting %s seconds", stime) - await asyncio.sleep(stime) - logging.info("wait complete: %s seconds", stime) - continue - ltype = int(_s.pop(False)) - if ltype == 11: - addr = ip(0) - else: - aval = _s.pop(False) - try: - addr = ip(int(aval)) - except ValueError: - addr = ip(aval) - oargs = [addr, ltype, int(_s.pop(False)), int(_s.pop(False))] - if what.casefold() == "add": + for action in next_action(args.actions): + _s = action.split(",") + what = _s.pop(False) + if what.casefold() == "wait": + stime = int(_s.pop(False)) + logging.info("waiting %s seconds", stime) + await asyncio.sleep(stime) + logging.info("wait complete: %s seconds", stime) + continue + ltype = int(_s.pop(False)) + if ltype == 11: + addr = ip(0) + else: + aval = _s.pop(False) + try: + addr = ip(int(aval)) + except ValueError: + addr = ip(aval) + oargs = [addr, ltype, int(_s.pop(False)), int(_s.pop(False))] + + if not await c.is_registered(oargs[1], oargs[2]): + await c.register_opaque_data_wait(oargs[1], oargs[2]) + + if what.casefold() == "add": + try: + b = bytes.fromhex(_s.pop(False)) + except IndexError: + b = b"" + logging.info("opaque data is %s octets", len(b)) + # Needs to be multiple of 4 in length + mod = len(b) % 4 + if mod: + b += b"\x00" * (4 - mod) + logging.info("opaque padding to %s octets", len(b)) + + await c.add_opaque_data(*oargs, b) + else: + assert what.casefold().startswith("del") + f = 0 + if len(_s) >= 1: try: - b = bytes.fromhex(_s.pop(False)) + f = int(_s.pop(False)) except IndexError: - b = b"" - logging.info("opaque data is %s octets", len(b)) - # Needs to be multiple of 4 in length - mod = len(b) % 4 - if mod: - b += b"\x00" * (4 - mod) - logging.info("opaque padding to %s octets", len(b)) - - await c.add_opaque_data(*oargs, b) - else: - assert what.casefold().startswith("del") - f = 0 - if len(_s) >= 1: - try: - f = int(_s.pop(False)) - except IndexError: - f = 0 - await c.delete_opaque_data(*oargs, f) - if args.exit: - return 0 + f = 0 + await c.delete_opaque_data(*oargs, f) + if not args.actions or args.exit: + return 0 except Exception as error: logging.error("async_main: unexpected error: %s", error, exc_info=True) return 2 @@ -1158,19 +1180,23 @@ async def async_main(args): def main(*args): ap = argparse.ArgumentParser(args) + ap.add_argument("--logtag", default="CLIENT", help="tag to identify log messages") ap.add_argument("--exit", action="store_true", help="Exit after commands") ap.add_argument("--server", default="localhost", help="OSPF API server") ap.add_argument("-v", "--verbose", action="store_true", help="be verbose") ap.add_argument( "actions", nargs="*", - help="(ADD|DEL),LSATYPE,[ADDR,],OTYPE,OID,[HEXDATA|DEL_FLAG]", + help="WAIT,SEC|(ADD|DEL),LSATYPE,[ADDR,],OTYPE,OID,[HEXDATA|DEL_FLAG]", ) args = ap.parse_args() level = logging.DEBUG if args.verbose else logging.INFO logging.basicConfig( - level=level, format="%(asctime)s %(levelname)s: CLIENT: %(name)s %(message)s" + level=level, + format="%(asctime)s %(levelname)s: {}: %(name)s %(message)s".format( + args.logtag + ), ) logging.info("ospfclient: starting") |
