mirror of
				https://git.proxmox.com/git/mirror_frr
				synced 2025-11-04 10:07:04 +00:00 
			
		
		
		
	- 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)
 |