mirror of
				https://git.proxmox.com/git/mirror_frr
				synced 2025-10-31 16:56:31 +00:00 
			
		
		
		
	 6efa8fd5c1
			
		
	
	
		6efa8fd5c1
		
	
	
	
	
		
			
			- also add ability of the apibin to process commands on stdin Signed-off-by: Christian Hopps <chopps@labn.net>
		
			
				
	
	
		
			1228 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1228 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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.
 | |
| #
 | |
| 
 | |
| import argparse
 | |
| import asyncio
 | |
| import errno
 | |
| import logging
 | |
| import socket
 | |
| import struct
 | |
| import sys
 | |
| from asyncio import Event, Lock
 | |
| from ipaddress import ip_address as ip
 | |
| 
 | |
| FMT_APIMSGHDR = ">BBHL"
 | |
| FMT_APIMSGHDR_SIZE = struct.calcsize(FMT_APIMSGHDR)
 | |
| 
 | |
| FMT_LSA_FILTER = ">HBB"  # + plus x"I" areas
 | |
| LSAF_ORIGIN_NON_SELF = 0
 | |
| LSAF_ORIGIN_SELF = 1
 | |
| LSAF_ORIGIN_ANY = 2
 | |
| 
 | |
| FMT_LSA_HEADER = ">HBBIILHH"
 | |
| FMT_LSA_HEADER_SIZE = struct.calcsize(FMT_LSA_HEADER)
 | |
| 
 | |
| # ------------------------
 | |
| # Messages to OSPF daemon.
 | |
| # ------------------------
 | |
| 
 | |
| MSG_REGISTER_OPAQUETYPE = 1
 | |
| MSG_UNREGISTER_OPAQUETYPE = 2
 | |
| MSG_REGISTER_EVENT = 3
 | |
| MSG_SYNC_LSDB = 4
 | |
| MSG_ORIGINATE_REQUEST = 5
 | |
| MSG_DELETE_REQUEST = 6
 | |
| MSG_SYNC_REACHABLE = 7
 | |
| MSG_SYNC_ISM = 8
 | |
| MSG_SYNC_NSM = 9
 | |
| MSG_SYNC_ROUTER_ID = 19
 | |
| 
 | |
| smsg_info = {
 | |
|     MSG_REGISTER_OPAQUETYPE: ("REGISTER_OPAQUETYPE", "BBxx"),
 | |
|     MSG_UNREGISTER_OPAQUETYPE: ("UNREGISTER_OPAQUETYPE", "BBxx"),
 | |
|     MSG_REGISTER_EVENT: ("REGISTER_EVENT", FMT_LSA_FILTER),
 | |
|     MSG_SYNC_LSDB: ("SYNC_LSDB", FMT_LSA_FILTER),
 | |
|     MSG_ORIGINATE_REQUEST: ("ORIGINATE_REQUEST", ">II" + FMT_LSA_HEADER[1:]),
 | |
|     MSG_DELETE_REQUEST: ("DELETE_REQUEST", ">IBBxBL"),
 | |
|     MSG_SYNC_REACHABLE: ("MSG_SYNC_REACHABLE", ""),
 | |
|     MSG_SYNC_ISM: ("MSG_SYNC_ISM", ""),
 | |
|     MSG_SYNC_NSM: ("MSG_SYNC_NSM", ""),
 | |
|     MSG_SYNC_ROUTER_ID: ("MSG_SYNC_ROUTER_ID", ""),
 | |
| }
 | |
| 
 | |
| # OSPF API MSG Delete Flag.
 | |
| OSPF_API_DEL_ZERO_LEN_LSA = 0x01  # send withdrawal with no LSA data
 | |
| 
 | |
| # --------------------------
 | |
| # Messages from OSPF daemon.
 | |
| # --------------------------
 | |
| 
 | |
| MSG_REPLY = 10
 | |
| MSG_READY_NOTIFY = 11
 | |
| MSG_LSA_UPDATE_NOTIFY = 12
 | |
| MSG_LSA_DELETE_NOTIFY = 13
 | |
| MSG_NEW_IF = 14
 | |
| MSG_DEL_IF = 15
 | |
| MSG_ISM_CHANGE = 16
 | |
| MSG_NSM_CHANGE = 17
 | |
| MSG_REACHABLE_CHANGE = 18
 | |
| MSG_ROUTER_ID_CHANGE = 20
 | |
| 
 | |
| amsg_info = {
 | |
|     MSG_REPLY: ("REPLY", "bxxx"),
 | |
|     MSG_READY_NOTIFY: ("READY_NOTIFY", ">BBxxI"),
 | |
|     MSG_LSA_UPDATE_NOTIFY: ("LSA_UPDATE_NOTIFY", ">IIBxxx" + FMT_LSA_HEADER[1:]),
 | |
|     MSG_LSA_DELETE_NOTIFY: ("LSA_DELETE_NOTIFY", ">IIBxxx" + FMT_LSA_HEADER[1:]),
 | |
|     MSG_NEW_IF: ("NEW_IF", ">II"),
 | |
|     MSG_DEL_IF: ("DEL_IF", ">I"),
 | |
|     MSG_ISM_CHANGE: ("ISM_CHANGE", ">IIBxxx"),
 | |
|     MSG_NSM_CHANGE: ("NSM_CHANGE", ">IIIBxxx"),
 | |
|     MSG_REACHABLE_CHANGE: ("REACHABLE_CHANGE", ">HH"),
 | |
|     MSG_ROUTER_ID_CHANGE: ("ROUTER_ID_CHANGE", ">I"),
 | |
| }
 | |
| 
 | |
| OSPF_API_OK = 0
 | |
| OSPF_API_NOSUCHINTERFACE = -1
 | |
| OSPF_API_NOSUCHAREA = -2
 | |
| OSPF_API_NOSUCHLSA = -3
 | |
| OSPF_API_ILLEGALLSATYPE = -4
 | |
| OSPF_API_OPAQUETYPEINUSE = -5
 | |
| OSPF_API_OPAQUETYPENOTREGISTERED = -6
 | |
| OSPF_API_NOTREADY = -7
 | |
| OSPF_API_NOMEMORY = -8
 | |
| OSPF_API_ERROR = -9
 | |
| OSPF_API_UNDEF = -10
 | |
| 
 | |
| msg_errname = {
 | |
|     OSPF_API_OK: "OSPF_API_OK",
 | |
|     OSPF_API_NOSUCHINTERFACE: "OSPF_API_NOSUCHINTERFACE",
 | |
|     OSPF_API_NOSUCHAREA: "OSPF_API_NOSUCHAREA",
 | |
|     OSPF_API_NOSUCHLSA: "OSPF_API_NOSUCHLSA",
 | |
|     OSPF_API_ILLEGALLSATYPE: "OSPF_API_ILLEGALLSATYPE",
 | |
|     OSPF_API_OPAQUETYPEINUSE: "OSPF_API_OPAQUETYPEINUSE",
 | |
|     OSPF_API_OPAQUETYPENOTREGISTERED: "OSPF_API_OPAQUETYPENOTREGISTERED",
 | |
|     OSPF_API_NOTREADY: "OSPF_API_NOTREADY",
 | |
|     OSPF_API_NOMEMORY: "OSPF_API_NOMEMORY",
 | |
|     OSPF_API_ERROR: "OSPF_API_ERROR",
 | |
|     OSPF_API_UNDEF: "OSPF_API_UNDEF",
 | |
| }
 | |
| 
 | |
| # msg_info = {**smsg_info, **amsg_info}
 | |
| msg_info = {}
 | |
| msg_info.update(smsg_info)
 | |
| msg_info.update(amsg_info)
 | |
| msg_name = {k: v[0] for k, v in msg_info.items()}
 | |
| msg_fmt = {k: v[1] for k, v in msg_info.items()}
 | |
| msg_size = {k: struct.calcsize(v) for k, v in msg_fmt.items()}
 | |
| 
 | |
| 
 | |
| def api_msgname(mt):
 | |
|     return msg_name.get(mt, str(mt))
 | |
| 
 | |
| 
 | |
| def api_errname(ecode):
 | |
|     return msg_errname.get(ecode, str(ecode))
 | |
| 
 | |
| 
 | |
| # -------------------
 | |
| # API Semantic Errors
 | |
| # -------------------
 | |
| 
 | |
| 
 | |
| class APIError(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class MsgTypeError(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class SeqNumError(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| # ---------
 | |
| # LSA Types
 | |
| # ---------
 | |
| 
 | |
| LSA_TYPE_UNKNOWN = 0
 | |
| LSA_TYPE_ROUTER = 1
 | |
| LSA_TYPE_NETWORK = 2
 | |
| LSA_TYPE_SUMMARY = 3
 | |
| LSA_TYPE_ASBR_SUMMARY = 4
 | |
| LSA_TYPE_AS_EXTERNAL = 5
 | |
| LSA_TYPE_GROUP_MEMBER = 6
 | |
| LSA_TYPE_AS_NSSA = 7
 | |
| LSA_TYPE_EXTERNAL_ATTRIBUTES = 8
 | |
| LSA_TYPE_OPAQUE_LINK = 9
 | |
| LSA_TYPE_OPAQUE_AREA = 10
 | |
| LSA_TYPE_OPAQUE_AS = 11
 | |
| 
 | |
| 
 | |
| def lsa_typename(lsa_type):
 | |
|     names = {
 | |
|         LSA_TYPE_ROUTER: "LSA:ROUTER",
 | |
|         LSA_TYPE_NETWORK: "LSA:NETWORK",
 | |
|         LSA_TYPE_SUMMARY: "LSA:SUMMARY",
 | |
|         LSA_TYPE_ASBR_SUMMARY: "LSA:ASBR_SUMMARY",
 | |
|         LSA_TYPE_AS_EXTERNAL: "LSA:AS_EXTERNAL",
 | |
|         LSA_TYPE_GROUP_MEMBER: "LSA:GROUP_MEMBER",
 | |
|         LSA_TYPE_AS_NSSA: "LSA:AS_NSSA",
 | |
|         LSA_TYPE_EXTERNAL_ATTRIBUTES: "LSA:EXTERNAL_ATTRIBUTES",
 | |
|         LSA_TYPE_OPAQUE_LINK: "LSA:OPAQUE_LINK",
 | |
|         LSA_TYPE_OPAQUE_AREA: "LSA:OPAQUE_AREA",
 | |
|         LSA_TYPE_OPAQUE_AS: "LSA:OPAQUE_AS",
 | |
|     }
 | |
|     return names.get(lsa_type, str(lsa_type))
 | |
| 
 | |
| 
 | |
| # ------------------------------
 | |
| # Interface State Machine States
 | |
| # ------------------------------
 | |
| 
 | |
| ISM_DEPENDUPON = 0
 | |
| ISM_DOWN = 1
 | |
| ISM_LOOPBACK = 2
 | |
| ISM_WAITING = 3
 | |
| ISM_POINTTOPOINT = 4
 | |
| ISM_DROTHER = 5
 | |
| ISM_BACKUP = 6
 | |
| ISM_DR = 7
 | |
| 
 | |
| 
 | |
| def ism_name(state):
 | |
|     names = {
 | |
|         ISM_DEPENDUPON: "ISM_DEPENDUPON",
 | |
|         ISM_DOWN: "ISM_DOWN",
 | |
|         ISM_LOOPBACK: "ISM_LOOPBACK",
 | |
|         ISM_WAITING: "ISM_WAITING",
 | |
|         ISM_POINTTOPOINT: "ISM_POINTTOPOINT",
 | |
|         ISM_DROTHER: "ISM_DROTHER",
 | |
|         ISM_BACKUP: "ISM_BACKUP",
 | |
|         ISM_DR: "ISM_DR",
 | |
|     }
 | |
|     return names.get(state, str(state))
 | |
| 
 | |
| 
 | |
| # -----------------------------
 | |
| # Neighbor State Machine States
 | |
| # -----------------------------
 | |
| 
 | |
| NSM_DEPENDUPON = 0
 | |
| NSM_DELETED = 1
 | |
| NSM_DOWN = 2
 | |
| NSM_ATTEMPT = 3
 | |
| NSM_INIT = 4
 | |
| NSM_TWOWAY = 5
 | |
| NSM_EXSTART = 6
 | |
| NSM_EXCHANGE = 7
 | |
| NSM_LOADING = 8
 | |
| NSM_FULL = 9
 | |
| 
 | |
| 
 | |
| def nsm_name(state):
 | |
|     names = {
 | |
|         NSM_DEPENDUPON: "NSM_DEPENDUPON",
 | |
|         NSM_DELETED: "NSM_DELETED",
 | |
|         NSM_DOWN: "NSM_DOWN",
 | |
|         NSM_ATTEMPT: "NSM_ATTEMPT",
 | |
|         NSM_INIT: "NSM_INIT",
 | |
|         NSM_TWOWAY: "NSM_TWOWAY",
 | |
|         NSM_EXSTART: "NSM_EXSTART",
 | |
|         NSM_EXCHANGE: "NSM_EXCHANGE",
 | |
|         NSM_LOADING: "NSM_LOADING",
 | |
|         NSM_FULL: "NSM_FULL",
 | |
|     }
 | |
|     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
 | |
| # --------------
 | |
| 
 | |
| 
 | |
| class OspfApiClient:
 | |
|     def __str__(self):
 | |
|         return "OspfApiClient({})".format(self.server)
 | |
| 
 | |
|     @staticmethod
 | |
|     def _get_bound_sockets(port):
 | |
|         s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
 | |
|         try:
 | |
|             s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 | |
|             # s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
 | |
|             s1.bind(("", port))
 | |
|             s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
 | |
|             try:
 | |
|                 s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 | |
|                 # s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
 | |
|                 s2.bind(("", port + 1))
 | |
|                 return s1, s2
 | |
|             except Exception:
 | |
|                 s2.close()
 | |
|                 raise
 | |
|         except Exception:
 | |
|             s1.close()
 | |
|             raise
 | |
| 
 | |
|     def __init__(self, server="localhost", handlers=None):
 | |
|         """A client connection to OSPF Daemon using the OSPF API
 | |
| 
 | |
|         The client object is not created in a connected state.  To connect to the server
 | |
|         the `connect` method should be called.  If an error is encountered when sending
 | |
|         messages to the server an exception will be raised and the connection will be
 | |
|         closed.  When this happens `connect` may be called again to restore the
 | |
|         connection.
 | |
| 
 | |
|         Args:
 | |
|             server: hostname or IP address of server default is "localhost"
 | |
|             handlers: dict of message handlers, the key is the API message
 | |
|                 type, the value is a function. The functions signature is:
 | |
|                 `handler(msg_type, msg, msg_extra, *params)`, where `msg` is the
 | |
|                 message data after the API header, `*params` will be the
 | |
|                 unpacked message values, and msg_extra are any bytes beyond the
 | |
|                 fixed parameters of the message.
 | |
|         Raises:
 | |
|             Will raise exceptions for failures with various `socket` modules
 | |
|             functions such as `socket.socket`, `socket.setsockopt`, `socket.bind`.
 | |
|         """
 | |
|         self._seq = 0
 | |
|         self._s = None
 | |
|         self._as = None
 | |
|         self._ls = None
 | |
|         self._ar = self._r = self._w = None
 | |
|         self.server = server
 | |
|         self.handlers = handlers if handlers is not None else dict()
 | |
|         self.write_lock = Lock()
 | |
| 
 | |
|         # try and get consecutive 2 ports
 | |
|         PORTSTART = 49152
 | |
|         PORTEND = 65534
 | |
|         for port in range(PORTSTART, PORTEND + 2, 2):
 | |
|             try:
 | |
|                 logging.debug("%s: binding to ports %s, %s", self, port, port + 1)
 | |
|                 self._s, self._ls = self._get_bound_sockets(port)
 | |
|                 break
 | |
|             except OSError as error:
 | |
|                 if error.errno != errno.EADDRINUSE or port == PORTEND:
 | |
|                     logging.warning("%s: binding port %s error %s", self, port, error)
 | |
|                     raise
 | |
|                 logging.debug("%s: ports %s, %s in use.", self, port, port + 1)
 | |
|         else:
 | |
|             assert False, "Should not reach this code execution point"
 | |
| 
 | |
|     async def _connect_locked(self):
 | |
|         logging.debug("%s: connect to OSPF API", self)
 | |
| 
 | |
|         loop = asyncio.get_event_loop()
 | |
| 
 | |
|         self._ls.listen()
 | |
|         try:
 | |
|             logging.debug("%s: connecting sync socket to server", self)
 | |
|             await loop.sock_connect(self._s, (self.server, 2607))
 | |
| 
 | |
|             logging.debug("%s: accepting connect from server", self)
 | |
|             self._as, _ = await loop.sock_accept(self._ls)
 | |
|         except Exception:
 | |
|             await self._close_locked()
 | |
|             raise
 | |
| 
 | |
|         logging.debug("%s: success", self)
 | |
|         self._r, self._w = await asyncio.open_connection(sock=self._s)
 | |
|         self._ar, _ = await asyncio.open_connection(sock=self._as)
 | |
|         self._seq = 1
 | |
| 
 | |
|     async def connect(self):
 | |
|         async with self.write_lock:
 | |
|             await self._connect_locked()
 | |
| 
 | |
|     @property
 | |
|     def closed(self):
 | |
|         "True if the connection is closed."
 | |
|         return self._seq == 0
 | |
| 
 | |
|     async def _close_locked(self):
 | |
|         logging.debug("%s: closing", self)
 | |
|         if self._s:
 | |
|             if self._w:
 | |
|                 self._w.close()
 | |
|                 await self._w.wait_closed()
 | |
|                 self._w = None
 | |
|             else:
 | |
|                 self._s.close()
 | |
|             self._s = None
 | |
|             self._r = None
 | |
|         assert self._w is None
 | |
|         if self._as:
 | |
|             self._as.close()
 | |
|             self._as = None
 | |
|             self._ar = None
 | |
|         if self._ls:
 | |
|             self._ls.close()
 | |
|             self._ls = None
 | |
|         self._seq = 0
 | |
| 
 | |
|     async def close(self):
 | |
|         async with self.write_lock:
 | |
|             await self._close_locked()
 | |
| 
 | |
|     @staticmethod
 | |
|     async def _msg_read(r, expseq=-1):
 | |
|         """Read an OSPF API message from the socket `r`
 | |
| 
 | |
|         Args:
 | |
|             r: socket to read msg from
 | |
|             expseq: sequence number to expect or -1 for any.
 | |
|         Raises:
 | |
|             Will raise exceptions for failures with various `socket` modules,
 | |
|             Additionally may raise SeqNumError if unexpected seqnum is received.
 | |
|         """
 | |
|         try:
 | |
|             mh = await r.readexactly(FMT_APIMSGHDR_SIZE)
 | |
|             v, mt, l, seq = struct.unpack(FMT_APIMSGHDR, mh)
 | |
|             if v != 1:
 | |
|                 raise Exception("received unexpected OSPF API version {}".format(v))
 | |
|             if expseq == -1:
 | |
|                 logging.debug("_msg_read: got seq: 0x%x on async read", seq)
 | |
|             elif seq != expseq:
 | |
|                 raise SeqNumError("rx {} != {}".format(seq, expseq))
 | |
|             msg = await r.readexactly(l) if l else b""
 | |
|             return mt, msg
 | |
|         except asyncio.IncompleteReadError:
 | |
|             raise EOFError
 | |
| 
 | |
|     async def msg_read(self):
 | |
|         """Read a message from the async notify channel.
 | |
| 
 | |
|         Raises:
 | |
|             May raise exceptions for failures with various `socket` modules.
 | |
|         """
 | |
|         return await OspfApiClient._msg_read(self._ar, -1)
 | |
| 
 | |
|     async def msg_send(self, mt, mp):
 | |
|         """Send a message to OSPF API and wait for error code reply.
 | |
| 
 | |
|         Args:
 | |
|             mt: the messaage type
 | |
|             mp: the message payload
 | |
|         Returns:
 | |
|             error: an OSPF_API_XXX error code, 0 for OK.
 | |
|         Raises:
 | |
|             Raises SeqNumError if the synchronous reply is the wrong sequence number;
 | |
|             MsgTypeError if the synchronous reply is not MSG_REPLY. Also,
 | |
|             may raise exceptions for failures with various `socket` modules,
 | |
| 
 | |
|             The connection will be closed.
 | |
|         """
 | |
|         logging.debug("SEND: %s: sending %s seq 0x%x", self, api_msgname(mt), self._seq)
 | |
|         mh = struct.pack(FMT_APIMSGHDR, 1, mt, len(mp), self._seq)
 | |
| 
 | |
|         seq = self._seq
 | |
|         self._seq = seq + 1
 | |
| 
 | |
|         try:
 | |
|             async with self.write_lock:
 | |
|                 self._w.write(mh + mp)
 | |
|                 await self._w.drain()
 | |
|                 mt, mp = await OspfApiClient._msg_read(self._r, seq)
 | |
| 
 | |
|             if mt != MSG_REPLY:
 | |
|                 raise MsgTypeError(
 | |
|                     "rx {} != {}".format(api_msgname(mt), api_msgname(MSG_REPLY))
 | |
|                 )
 | |
| 
 | |
|             return struct.unpack(msg_fmt[MSG_REPLY], mp)[0]
 | |
|         except Exception:
 | |
|             # We've written data with a sequence number
 | |
|             await self.close()
 | |
|             raise
 | |
| 
 | |
|     async def msg_send_raises(self, mt, mp=b"\x00" * 4):
 | |
|         """Send a message to OSPF API and wait for error code reply.
 | |
| 
 | |
|         Args:
 | |
|             mt: the messaage type
 | |
|             mp: the message payload
 | |
|         Raises:
 | |
|             APIError if the server replies with an error.
 | |
| 
 | |
|             Also may raise exceptions for failures with various `socket` modules,
 | |
|             as well as MsgTypeError if the synchronous reply is incorrect.
 | |
|             The connection will be closed for these non-API error exceptions.
 | |
|         """
 | |
|         ecode = await self.msg_send(mt, mp)
 | |
|         if ecode:
 | |
|             raise APIError("{} error {}".format(api_msgname(mt), api_errname(ecode)))
 | |
| 
 | |
|     async def handle_async_msg(self, mt, msg):
 | |
|         if mt not in msg_fmt:
 | |
|             logging.debug("RECV: %s: unknown async msg type %s", self, mt)
 | |
|             return
 | |
| 
 | |
|         fmt = msg_fmt[mt]
 | |
|         sz = msg_size[mt]
 | |
|         tup = struct.unpack(fmt, msg[:sz])
 | |
|         extra = msg[sz:]
 | |
| 
 | |
|         if mt not in self.handlers:
 | |
|             logging.debug(
 | |
|                 "RECV: %s: no handlers for msg type %s", self, api_msgname(mt)
 | |
|             )
 | |
|             return
 | |
| 
 | |
|         logging.debug("RECV: %s: calling handler for %s", self, api_msgname(mt))
 | |
|         await self.handlers[mt](mt, msg, extra, *tup)
 | |
| 
 | |
|     #
 | |
|     # Client to Server Messaging
 | |
|     #
 | |
|     @staticmethod
 | |
|     def lsa_type_mask(*lsa_types):
 | |
|         "Return a 16 bit mask for each LSA type passed."
 | |
|         if not lsa_types:
 | |
|             return 0xFFFF
 | |
|         mask = 0
 | |
|         for t in lsa_types:
 | |
|             assert 0 < t < 16, "LSA type {} out of range [1, 15]".format(t)
 | |
|             mask |= 1 << t
 | |
|         return mask
 | |
| 
 | |
|     @staticmethod
 | |
|     def lsa_filter(origin, areas, lsa_types):
 | |
|         """Return an LSA filter.
 | |
| 
 | |
|         Return the filter message bytes based on `origin` the `areas` list and the LSAs
 | |
|         types in the `lsa_types` list.
 | |
|         """
 | |
|         mask = OspfApiClient.lsa_type_mask(*lsa_types)
 | |
|         narea = len(areas)
 | |
|         fmt = FMT_LSA_FILTER + ("{}I".format(narea) if narea else "")
 | |
|         # lsa type mask, origin, number of areas, each area
 | |
|         return struct.pack(fmt, mask, origin, narea, *areas)
 | |
| 
 | |
|     async def req_lsdb_sync(self):
 | |
|         "Register for all LSA notifications and request an LSDB synchronoization."
 | |
|         logging.debug("SEND: %s: request LSDB events", self)
 | |
|         mp = OspfApiClient.lsa_filter(LSAF_ORIGIN_ANY, [], [])
 | |
|         await self.msg_send_raises(MSG_REGISTER_EVENT, mp)
 | |
| 
 | |
|         logging.debug("SEND: %s: request LSDB sync", self)
 | |
|         await self.msg_send_raises(MSG_SYNC_LSDB, mp)
 | |
| 
 | |
|     async def req_reachable_routers(self):
 | |
|         "Request a dump of all reachable routers."
 | |
|         logging.debug("SEND: %s: request reachable changes", self)
 | |
|         await self.msg_send_raises(MSG_SYNC_REACHABLE)
 | |
| 
 | |
|     async def req_ism_states(self):
 | |
|         "Request a dump of the current ISM states of all interfaces."
 | |
|         logging.debug("SEND: %s: request ISM changes", self)
 | |
|         await self.msg_send_raises(MSG_SYNC_ISM)
 | |
| 
 | |
|     async def req_nsm_states(self):
 | |
|         "Request a dump of the current NSM states of all neighbors."
 | |
|         logging.debug("SEND: %s: request NSM changes", self)
 | |
|         await self.msg_send_raises(MSG_SYNC_NSM)
 | |
| 
 | |
|     async def req_router_id_sync(self):
 | |
|         "Request a dump of the current NSM states of all neighbors."
 | |
|         logging.debug("SEND: %s: request router ID sync", self)
 | |
|         await self.msg_send_raises(MSG_SYNC_ROUTER_ID)
 | |
| 
 | |
| 
 | |
| class OspfOpaqueClient(OspfApiClient):
 | |
|     """A client connection to OSPF Daemon for manipulating Opaque LSA data.
 | |
| 
 | |
|     The client object is not created in a connected state.  To connect to the server
 | |
|     the `connect` method should be called.  If an error is encountered when sending
 | |
|     messages to the server an exception will be raised and the connection will be
 | |
|     closed.  When this happens `connect` may be called again to restore the
 | |
|     connection.
 | |
| 
 | |
|     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", wait_ready=False):
 | |
|         handlers = {
 | |
|             MSG_LSA_UPDATE_NOTIFY: self._lsa_change_msg,
 | |
|             MSG_LSA_DELETE_NOTIFY: self._lsa_change_msg,
 | |
|             MSG_NEW_IF: self._if_msg,
 | |
|             MSG_DEL_IF: self._if_msg,
 | |
|             MSG_ISM_CHANGE: self._if_change_msg,
 | |
|             MSG_NSM_CHANGE: self._nbr_change_msg,
 | |
|             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.wait_ready = wait_ready
 | |
|         self.ready_lock = Lock() if wait_ready else WithNothing()
 | |
|         self.ready_cond = {
 | |
|             LSA_TYPE_OPAQUE_LINK: {},
 | |
|             LSA_TYPE_OPAQUE_AREA: {},
 | |
|             LSA_TYPE_OPAQUE_AS: {},
 | |
|         }
 | |
|         self.router_id = ip(0)
 | |
|         self.router_id_change_cb = None
 | |
| 
 | |
|         self.lsid_seq_num = {}
 | |
| 
 | |
|         self.lsa_change_cb = None
 | |
|         self.opaque_change_cb = {}
 | |
| 
 | |
|         self.reachable_routers = set()
 | |
|         self.reachable_change_cb = None
 | |
| 
 | |
|         self.if_area = {}
 | |
|         self.ism_states = {}
 | |
|         self.ism_change_cb = None
 | |
| 
 | |
|         self.nsm_states = {}
 | |
|         self.nsm_change_cb = None
 | |
| 
 | |
|     async def _register_opaque_data(self, lsa_type, otype):
 | |
|         async with self.ready_lock:
 | |
|             cond = self.ready_cond[lsa_type].get(otype)
 | |
|             assert cond is None, "multiple registers for {} opaque-type {}".format(
 | |
|                 lsa_typename(lsa_type), otype
 | |
|             )
 | |
| 
 | |
|             logging.debug("register %s opaque-type %s", lsa_typename(lsa_type), otype)
 | |
| 
 | |
|             mt = MSG_REGISTER_OPAQUETYPE
 | |
|             mp = struct.pack(msg_fmt[mt], lsa_type, otype)
 | |
|             await self.msg_send_raises(mt, mp)
 | |
| 
 | |
|             # 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:
 | |
|             logging.debug("entering async msg handling loop")
 | |
|             while True:
 | |
|                 mt, msg = await self.msg_read()
 | |
|                 if mt in amsg_info:
 | |
|                     await self.handle_async_msg(mt, msg)
 | |
|                 else:
 | |
|                     mts = api_msgname(mt)
 | |
|                     logging.warning(
 | |
|                         "ignoring unexpected msg: %s len: %s", mts, len(msg)
 | |
|                     )
 | |
|         except EOFError:
 | |
|             logging.info("Got EOF from OSPF API server on async notify socket")
 | |
|             return 2
 | |
| 
 | |
|     @staticmethod
 | |
|     def _opaque_args(lsa_type, otype, oid, mp):
 | |
|         lsid = (otype << 24) | oid
 | |
|         return 0, 0, lsa_type, lsid, 0, 0, 0, FMT_LSA_HEADER_SIZE + len(mp)
 | |
| 
 | |
|     @staticmethod
 | |
|     def _make_opaque_lsa(lsa_type, otype, oid, mp):
 | |
|         # /* Make a new LSA from parameters */
 | |
|         lsa = struct.pack(
 | |
|             FMT_LSA_HEADER, *OspfOpaqueClient._opaque_args(lsa_type, otype, oid, mp)
 | |
|         )
 | |
|         lsa += mp
 | |
|         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:
 | |
|             e = "area {}".format(ip(addr))
 | |
|         else:
 | |
|             e = ""
 | |
|         logging.info(
 | |
|             "RECV: %s ready notify for %s opaque-type %s%s",
 | |
|             self,
 | |
|             lsa_typename(lsa_type),
 | |
|             otype,
 | |
|             e,
 | |
|         )
 | |
| 
 | |
|         # Signal all waiting senders they can send now.
 | |
|         async with self.ready_lock:
 | |
|             cond = self.ready_cond[lsa_type].get(otype)
 | |
|             self.ready_cond[lsa_type][otype] = True
 | |
| 
 | |
|         if cond is True:
 | |
|             logging.warning(
 | |
|                 "RECV: dup ready received for %s opaque-type %s",
 | |
|                 lsa_typename(lsa_type),
 | |
|                 otype,
 | |
|             )
 | |
|         elif cond:
 | |
|             for evt in cond:
 | |
|                 evt.set()
 | |
| 
 | |
|     async def _if_msg(self, mt, msg, extra, *args):
 | |
|         if mt == MSG_NEW_IF:
 | |
|             ifaddr, aid = args
 | |
|         else:
 | |
|             assert mt == MSG_DEL_IF
 | |
|             ifaddr, aid = args[0], 0
 | |
|         logging.info(
 | |
|             "RECV: %s ifaddr %s areaid %s", api_msgname(mt), ip(ifaddr), ip(aid)
 | |
|         )
 | |
| 
 | |
|     async def _if_change_msg(self, mt, msg, extra, ifaddr, aid, state):
 | |
|         ifaddr = ip(ifaddr)
 | |
|         aid = ip(aid)
 | |
| 
 | |
|         logging.info(
 | |
|             "RECV: %s ifaddr %s areaid %s state %s",
 | |
|             api_msgname(mt),
 | |
|             ifaddr,
 | |
|             aid,
 | |
|             ism_name(state),
 | |
|         )
 | |
| 
 | |
|         self.if_area[ifaddr] = aid
 | |
|         self.ism_states[ifaddr] = state
 | |
| 
 | |
|         if self.ism_change_cb:
 | |
|             self.ism_change_cb(ifaddr, aid, state)
 | |
| 
 | |
|     async def _nbr_change_msg(self, mt, msg, extra, ifaddr, nbraddr, router_id, state):
 | |
|         ifaddr = ip(ifaddr)
 | |
|         nbraddr = ip(nbraddr)
 | |
|         router_id = ip(router_id)
 | |
| 
 | |
|         logging.info(
 | |
|             "RECV: %s ifaddr %s nbraddr %s router_id %s state %s",
 | |
|             api_msgname(mt),
 | |
|             ifaddr,
 | |
|             nbraddr,
 | |
|             router_id,
 | |
|             nsm_name(state),
 | |
|         )
 | |
| 
 | |
|         if ifaddr not in self.nsm_states:
 | |
|             self.nsm_states[ifaddr] = {}
 | |
|         self.nsm_states[ifaddr][(nbraddr, router_id)] = state
 | |
| 
 | |
|         if self.nsm_change_cb:
 | |
|             self.nsm_change_cb(ifaddr, nbraddr, router_id, state)
 | |
| 
 | |
|     async def _lsa_change_msg(self, mt, msg, extra, ifaddr, aid, is_self, *ls_header):
 | |
|         (
 | |
|             lsa_age,  # ls_age,
 | |
|             _,  # ls_options,
 | |
|             lsa_type,
 | |
|             ls_id,
 | |
|             _,  # ls_adv_router,
 | |
|             ls_seq,
 | |
|             _,  # ls_cksum,
 | |
|             ls_len,
 | |
|         ) = ls_header
 | |
| 
 | |
|         otype = (ls_id >> 24) & 0xFF
 | |
| 
 | |
|         if mt == MSG_LSA_UPDATE_NOTIFY:
 | |
|             ts = "update"
 | |
|         else:
 | |
|             assert mt == MSG_LSA_DELETE_NOTIFY
 | |
|             ts = "delete"
 | |
| 
 | |
|         logging.info(
 | |
|             "RECV: LSA %s msg for LSA %s in area %s seq 0x%x len %s age %s",
 | |
|             ts,
 | |
|             ip(ls_id),
 | |
|             ip(aid),
 | |
|             ls_seq,
 | |
|             ls_len,
 | |
|             lsa_age,
 | |
|         )
 | |
|         idx = (lsa_type, otype)
 | |
| 
 | |
|         pre_lsa_size = msg_size[mt] - FMT_LSA_HEADER_SIZE
 | |
|         lsa = msg[pre_lsa_size:]
 | |
| 
 | |
|         if idx in self.opaque_change_cb:
 | |
|             self.opaque_change_cb[idx](mt, ifaddr, aid, ls_header, extra, lsa)
 | |
| 
 | |
|         if self.lsa_change_cb:
 | |
|             self.lsa_change_cb(mt, ifaddr, aid, ls_header, extra, lsa)
 | |
| 
 | |
|     async def _reachable_msg(self, mt, msg, extra, nadd, nremove):
 | |
|         router_ids = struct.unpack(">{}I".format(nadd + nremove), extra)
 | |
|         router_ids = [ip(x) for x in router_ids]
 | |
|         logging.info(
 | |
|             "RECV: %s added %s removed %s",
 | |
|             api_msgname(mt),
 | |
|             router_ids[:nadd],
 | |
|             router_ids[nadd:],
 | |
|         )
 | |
|         self.reachable_routers |= set(router_ids[:nadd])
 | |
|         self.reachable_routers -= set(router_ids[nadd:])
 | |
|         logging.info("RECV: %s new set %s", api_msgname(mt), self.reachable_routers)
 | |
| 
 | |
|         if self.reachable_change_cb:
 | |
|             logging.info("RECV: %s calling callback", api_msgname(mt))
 | |
|             await self.reachable_change_cb(router_ids[:nadd], router_ids[nadd:])
 | |
| 
 | |
|     async def _router_id_msg(self, mt, msg, extra, router_id):
 | |
|         router_id = ip(router_id)
 | |
|         logging.info("RECV: %s router ID %s", api_msgname(mt), router_id)
 | |
|         old_router_id = self.router_id
 | |
|         if old_router_id == router_id:
 | |
|             return
 | |
| 
 | |
|         self.router_id = router_id
 | |
|         logging.info(
 | |
|             "RECV: %s new router ID %s older router ID %s",
 | |
|             api_msgname(mt),
 | |
|             router_id,
 | |
|             old_router_id,
 | |
|         )
 | |
| 
 | |
|         if self.router_id_change_cb:
 | |
|             logging.info("RECV: %s calling callback", api_msgname(mt))
 | |
|             await self.router_id_change_cb(router_id, old_router_id)
 | |
| 
 | |
|     async def add_opaque_data(self, addr, lsa_type, otype, oid, data):
 | |
|         """Add an instance of opaque data.
 | |
| 
 | |
|         Add an instance of opaque data. This call will register for the given
 | |
|         LSA and opaque type if not already done.
 | |
| 
 | |
|         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
 | |
|             oid: (3 octets) ID of this opaque data
 | |
|             data: the opaque data
 | |
|         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
 | |
|         elif lsa_type == LSA_TYPE_OPAQUE_AREA:
 | |
|             ifaddr, aid = 0, int(addr)
 | |
|         else:
 | |
|             assert lsa_type == LSA_TYPE_OPAQUE_AS
 | |
|             ifaddr, aid = 0, 0
 | |
| 
 | |
|         mt = MSG_ORIGINATE_REQUEST
 | |
|         msg = struct.pack(
 | |
|             msg_fmt[mt],
 | |
|             ifaddr,
 | |
|             aid,
 | |
|             *OspfOpaqueClient._opaque_args(lsa_type, otype, oid, data),
 | |
|         )
 | |
|         msg += data
 | |
|         await self.msg_send_raises(mt, msg)
 | |
| 
 | |
|     async def delete_opaque_data(self, addr, lsa_type, otype, oid, flags=0):
 | |
|         """Delete an instance of opaque data.
 | |
| 
 | |
|         Delete an instance of opaque data. This call will register for the given
 | |
|         LSA and opaque type if not already done.
 | |
| 
 | |
|         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.
 | |
|             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`
 | |
|         """
 | |
|         assert self.ready_cond.get(lsa_type, {}).get(otype) is True, "Not Registered!"
 | |
| 
 | |
|         mt = MSG_DELETE_REQUEST
 | |
|         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.
 | |
| 
 | |
|         The application should wait for the async notificaiton that the server is
 | |
|         ready to advertise the given opaque data type. The API currently only allows
 | |
|         a single "owner" of each unique (lsa_type,otype). To wait call `wait_opaque_ready`
 | |
| 
 | |
|         Args:
 | |
|             lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS}
 | |
|             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:
 | |
| 
 | |
|                 `callback(msg_type, ifaddr, area_id, lsa_header, data, lsa)`
 | |
| 
 | |
|                 Args:
 | |
|                     msg_type: MSG_LSA_UPDATE_NOTIFY or MSG_LSA_DELETE_NOTIFY
 | |
|                     ifaddr: integer identifying an interface (by IP address)
 | |
|                     area_id: integer identifying an area
 | |
|                     lsa_header: the LSA header as an unpacked tuple (fmt: ">HBBIILHH")
 | |
|                     data: the opaque data that follows the LSA header
 | |
|                     lsa: the octets of the full lsa
 | |
|         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:
 | |
|             logging.warning(
 | |
|                 "OSPFCLIENT: register: removing callback for %s opaque-type %s",
 | |
|                 lsa_typename(lsa_type),
 | |
|                 otype,
 | |
|             )
 | |
|             del self.opaque_change_cb[(lsa_type, otype)]
 | |
| 
 | |
|         await self._register_opaque_data(lsa_type, otype)
 | |
| 
 | |
|     async def wait_opaque_ready(self, lsa_type, otype):
 | |
|         async with self.ready_lock:
 | |
|             cond = self.ready_cond[lsa_type].get(otype)
 | |
|             if cond is True:
 | |
|                 return
 | |
| 
 | |
|             assert self.wait_ready
 | |
| 
 | |
|             logging.debug(
 | |
|                 "waiting for ready %s opaque-type %s", lsa_typename(lsa_type), otype
 | |
|             )
 | |
| 
 | |
|             if not cond:
 | |
|                 cond = self.ready_cond[lsa_type][otype] = []
 | |
| 
 | |
|             evt = Event()
 | |
|             cond.append(evt)
 | |
| 
 | |
|         await evt.wait()
 | |
|         logging.debug("READY for %s opaque-type %s", lsa_typename(lsa_type), otype)
 | |
| 
 | |
|     async def register_opaque_data_wait(self, lsa_type, otype, callback=None):
 | |
|         """Register intent to advertise opaque data and wait for ready.
 | |
| 
 | |
|         The API currently only allows a single "owner" of each unique (lsa_type,otype).
 | |
| 
 | |
|         Args:
 | |
|             lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS}
 | |
|             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:
 | |
| 
 | |
|                 `callback(msg_type, ifaddr, area_id, lsa_header, data, lsa)`
 | |
| 
 | |
|                 Args:
 | |
|                     msg_type: MSG_LSA_UPDATE_NOTIFY or MSG_LSA_DELETE_NOTIFY
 | |
|                     ifaddr: integer identifying an interface (by IP address)
 | |
|                     area_id: integer identifying an area
 | |
|                     lsa_header: the LSA header as an unpacked tuple (fmt: ">HBBIILHH")
 | |
|                     data: the opaque data that follows the LSA header
 | |
|                     lsa: the octets of the full lsa
 | |
|         Raises:
 | |
| 
 | |
|             See `msg_send_raises`
 | |
|         """
 | |
|         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.
 | |
| 
 | |
|         This will also cause the server to flush/delete all opaque data of
 | |
|         the given (lsa_type,otype).
 | |
| 
 | |
|         Args:
 | |
|             lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS}
 | |
|             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)]
 | |
| 
 | |
|         mt = MSG_UNREGISTER_OPAQUETYPE
 | |
|         mp = struct.pack(msg_fmt[mt], lsa_type, otype)
 | |
|         await self.msg_send_raises(mt, mp)
 | |
| 
 | |
|     async def monitor_lsa(self, callback=None):
 | |
|         """Monitor changes to LSAs.
 | |
| 
 | |
|         Args:
 | |
|             callback: if given, callback will be called when changes are received for
 | |
|                 any LSA. The callback signature is:
 | |
| 
 | |
|                 `callback(msg_type, ifaddr, area_id, lsa_header, extra, lsa)`
 | |
| 
 | |
|                 Args:
 | |
|                     msg_type: MSG_LSA_UPDATE_NOTIFY or MSG_LSA_DELETE_NOTIFY
 | |
|                     ifaddr: integer identifying an interface (by IP address)
 | |
|                     area_id: integer identifying an area
 | |
|                     lsa_header: the LSA header as an unpacked tuple (fmt: ">HBBIILHH")
 | |
|                     extra: the octets that follow the LSA header
 | |
|                     lsa: the octets of the full lsa
 | |
|         """
 | |
|         self.lsa_change_cb = callback
 | |
|         await self.req_lsdb_sync()
 | |
| 
 | |
|     async def monitor_reachable(self, callback=None):
 | |
|         """Monitor the set of reachable routers.
 | |
| 
 | |
|         The property `reachable_routers` contains the set() of reachable router IDs
 | |
|         as integers. This set is updated prior to calling the `callback`
 | |
| 
 | |
|         Args:
 | |
|             callback: callback will be called when the set of reachable
 | |
|                 routers changes. The callback signature is:
 | |
| 
 | |
|                 `callback(added, removed)`
 | |
| 
 | |
|                 Args:
 | |
|                     added: list of integer router IDs being added
 | |
|                     removed: list of integer router IDs being removed
 | |
|         """
 | |
|         self.reachable_change_cb = callback
 | |
|         await self.req_reachable_routers()
 | |
| 
 | |
|     async def monitor_ism(self, callback=None):
 | |
|         """Monitor the state of OSPF enabled interfaces.
 | |
| 
 | |
|         Args:
 | |
|             callback: callback will be called when an interface changes state.
 | |
|                 The callback signature is:
 | |
| 
 | |
|                 `callback(ifaddr, area_id, state)`
 | |
| 
 | |
|                 Args:
 | |
|                     ifaddr: integer identifying an interface (by IP address)
 | |
|                     area_id: integer identifying an area
 | |
|                     state: ISM_*
 | |
|         """
 | |
|         self.ism_change_cb = callback
 | |
|         await self.req_ism_states()
 | |
| 
 | |
|     async def monitor_nsm(self, callback=None):
 | |
|         """Monitor the state of OSPF neighbors.
 | |
| 
 | |
|         Args:
 | |
|             callback: callback will be called when a neighbor changes state.
 | |
|                 The callback signature is:
 | |
| 
 | |
|                 `callback(ifaddr, nbr_addr, router_id, state)`
 | |
| 
 | |
|                 Args:
 | |
|                     ifaddr: integer identifying an interface (by IP address)
 | |
|                     nbr_addr: integer identifying neighbor by IP address
 | |
|                     router_id: integer identifying neighbor router ID
 | |
|                     state: NSM_*
 | |
|         """
 | |
|         self.nsm_change_cb = callback
 | |
|         await self.req_nsm_states()
 | |
| 
 | |
|     async def monitor_router_id(self, callback=None):
 | |
|         """Monitor the OSPF router ID.
 | |
| 
 | |
|         The property `router_id` contains the OSPF urouter ID.
 | |
|         This value is updated prior to calling the `callback`
 | |
| 
 | |
|         Args:
 | |
|             callback: callback will be called when the router ID changes.
 | |
|                 The callback signature is:
 | |
| 
 | |
|                 `callback(new_router_id, old_router_id)`
 | |
| 
 | |
|                 Args:
 | |
|                     new_router_id: the new router ID
 | |
|                     old_router_id: the old router ID
 | |
|         """
 | |
|         self.router_id_change_cb = callback
 | |
|         await self.req_router_id_sync()
 | |
| 
 | |
| 
 | |
| # ================
 | |
| # 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):
 | |
|     c = OspfOpaqueClient(args.server)
 | |
|     await c.connect()
 | |
| 
 | |
|     try:
 | |
|         # Start handling async messages from server.
 | |
|         if sys.version_info[1] > 6:
 | |
|             asyncio.create_task(c._handle_msg_loop())
 | |
|         else:
 | |
|             asyncio.get_event_loop().create_task(c._handle_msg_loop())
 | |
| 
 | |
|         await c.req_lsdb_sync()
 | |
|         await c.req_reachable_routers()
 | |
|         await c.req_ism_states()
 | |
|         await c.req_nsm_states()
 | |
| 
 | |
|         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:
 | |
|                         f = int(_s.pop(False))
 | |
|                     except IndexError:
 | |
|                         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
 | |
| 
 | |
|     try:
 | |
|         logging.info("Sleeping forever")
 | |
|         while True:
 | |
|             await asyncio.sleep(120)
 | |
|     except EOFError:
 | |
|         logging.info("Got EOF from OSPF API server on async notify socket")
 | |
|         return 2
 | |
| 
 | |
| 
 | |
| 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="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: {}: %(name)s %(message)s".format(
 | |
|             args.logtag
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     logging.info("ospfclient: starting")
 | |
| 
 | |
|     status = 3
 | |
|     try:
 | |
|         if sys.version_info[1] > 6:
 | |
|             # python >= 3.7
 | |
|             status = asyncio.run(async_main(args))
 | |
|         else:
 | |
|             loop = asyncio.get_event_loop()
 | |
|             try:
 | |
|                 status = loop.run_until_complete(async_main(args))
 | |
|             finally:
 | |
|                 loop.close()
 | |
|     except KeyboardInterrupt:
 | |
|         logging.info("Exiting, received KeyboardInterrupt in main")
 | |
|     except Exception as error:
 | |
|         logging.info("Exiting, unexpected exception %s", error, exc_info=True)
 | |
|     else:
 | |
|         logging.info("ospfclient: clean exit")
 | |
| 
 | |
|     return status
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     exit_status = main()
 | |
|     sys.exit(exit_status)
 |