summaryrefslogtreecommitdiff
path: root/ospfclient/ospfclient.py
diff options
context:
space:
mode:
Diffstat (limited to 'ospfclient/ospfclient.py')
-rwxr-xr-xospfclient/ospfclient.py208
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")