From 3c012f7ed1ca57bfec7bdd033ae9ebc0365f0fae Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Sat, 11 Sep 2021 00:47:01 -0700 Subject: [PATCH] Start output plugin feature --- hyperglass/main.py | 8 +--- hyperglass/models/config/devices.py | 12 +++-- hyperglass/plugins/__init__.py | 12 +++++ hyperglass/plugins/_builtin/__init__.py | 6 +++ hyperglass/plugins/_builtin/remove_command.py | 23 +++++++++ hyperglass/plugins/_output.py | 29 ++++++++++++ hyperglass/plugins/_register.py | 47 +++++++++++++++++++ hyperglass/plugins/main.py | 18 +++++++ 8 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 hyperglass/plugins/__init__.py create mode 100644 hyperglass/plugins/_builtin/__init__.py create mode 100644 hyperglass/plugins/_builtin/remove_command.py create mode 100644 hyperglass/plugins/_output.py create mode 100644 hyperglass/plugins/_register.py create mode 100644 hyperglass/plugins/main.py diff --git a/hyperglass/main.py b/hyperglass/main.py index ecd48ee..231f14c 100644 --- a/hyperglass/main.py +++ b/hyperglass/main.py @@ -14,6 +14,7 @@ from gunicorn.glogging import Logger # type: ignore # Local from .log import log, setup_lib_logging +from .plugins import init_plugins from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__ from .util.frontend import get_node_version @@ -120,15 +121,10 @@ def on_starting(server: Arbiter): required = ".".join((str(v) for v in MIN_PYTHON_VERSION)) log.info("Python {} detected ({} required)", python_version, required) - async def runner(): - # Standard Library - from asyncio import gather - - await gather(build_ui(), cache_config()) - check_redis_instance() aiorun(build_ui()) cache_config() + init_plugins() log.success( "Started hyperglass {v} on http://{h}:{p} with {w} workers", diff --git a/hyperglass/models/config/devices.py b/hyperglass/models/config/devices.py index 256007f..36ff07a 100644 --- a/hyperglass/models/config/devices.py +++ b/hyperglass/models/config/devices.py @@ -103,20 +103,22 @@ class Device(HyperglassModel, extra="allow"): return device_id, {"name": display_name, "display_name": None, **values} - def _validate_directive_attrs(self) -> None: - - # Get all commands associated with the device. - commands = [ + @property + def directive_commands(self) -> List[str]: + """Get all commands associated with the device.""" + return [ command for directive in self.commands for rule in directive.rules for command in rule.commands ] + def _validate_directive_attrs(self) -> None: + # Set of all keys except for built-in key `target`. keys = { key - for group in [get_fmt_keys(command) for command in commands] + for group in [get_fmt_keys(command) for command in self.directive_commands] for key in group if key != "target" } diff --git a/hyperglass/plugins/__init__.py b/hyperglass/plugins/__init__.py new file mode 100644 index 0000000..10caf45 --- /dev/null +++ b/hyperglass/plugins/__init__.py @@ -0,0 +1,12 @@ +"""hyperglass Plugins.""" + +# Local +from .main import init_plugins +from ._output import OutputPlugin +from ._register import register_output_plugin + +__all__ = ( + "OutputPlugin", + "register_output_plugin", + "init_plugins", +) diff --git a/hyperglass/plugins/_builtin/__init__.py b/hyperglass/plugins/_builtin/__init__.py new file mode 100644 index 0000000..bcf6666 --- /dev/null +++ b/hyperglass/plugins/_builtin/__init__.py @@ -0,0 +1,6 @@ +"""Built-in hyperglass plugins.""" + +# Local +from .remove_command import RemoveCommand + +__all__ = ("RemoveCommand",) diff --git a/hyperglass/plugins/_builtin/remove_command.py b/hyperglass/plugins/_builtin/remove_command.py new file mode 100644 index 0000000..25a63f0 --- /dev/null +++ b/hyperglass/plugins/_builtin/remove_command.py @@ -0,0 +1,23 @@ +"""Remove anything before the command if found in output.""" + +# Project +from hyperglass.models.config.devices import Device + +# Local +from .._output import OutputPlugin + + +class RemoveCommand(OutputPlugin): + """Remove anything before the command if found in output.""" + + def process(self, device_output: str, device: Device) -> str: + """Remove anything before the command if found in output.""" + output = device_output.strip().split("\n") + + for command in device.directive_commands: + for line in output: + if command in line: + idx = output.index(line) + 1 + output = output[idx:] + + return "\n".join(output) diff --git a/hyperglass/plugins/_output.py b/hyperglass/plugins/_output.py new file mode 100644 index 0000000..c355c38 --- /dev/null +++ b/hyperglass/plugins/_output.py @@ -0,0 +1,29 @@ +"""Device output plugins.""" + +# Standard Library +import abc + +# Project +from hyperglass.models import HyperglassModel +from hyperglass.models.config.devices import Device + + +class OutputPlugin(HyperglassModel, abc.ABC): + """Plugin to interact with device command output.""" + + def __eq__(self, other: "OutputPlugin"): + """Other plugin is equal to this plugin.""" + return other and self.__repr_name__ == other.__repr_name__ + + def __ne__(self, other: "OutputPlugin"): + """Other plugin is not equal to this plugin.""" + return not self.__eq__(other) + + def __hash__(self) -> int: + """Create a hashed representation of this plugin's name.""" + return hash(self.__repr_name__) + + @abc.abstractmethod + def process(self, device_output: str, device: Device) -> str: + """Process/manipulate output from a device.""" + pass diff --git a/hyperglass/plugins/_register.py b/hyperglass/plugins/_register.py new file mode 100644 index 0000000..6227ac3 --- /dev/null +++ b/hyperglass/plugins/_register.py @@ -0,0 +1,47 @@ +"""Plugin registration.""" + +# Standard Library +import json +import codecs +import pickle +from typing import List, Generator + +# Project +from hyperglass.log import log +from hyperglass.cache import SyncCache +from hyperglass.configuration import REDIS_CONFIG, params + +# Local +from ._output import OutputPlugin + +CACHE = SyncCache(db=params.cache.database, **REDIS_CONFIG) + + +def get_plugins() -> Generator[OutputPlugin, None, None]: + """Retrieve plugins from cache.""" + # Retrieve plugins from cache. + raw = CACHE.get("hyperglass.plugins.output") + if isinstance(raw, List): + for plugin in raw: + yield pickle.loads(codecs.decode(plugin.encode(), "base64")) + + +def add_plugin(plugin: OutputPlugin) -> None: + """Add a plugin to currently active plugins.""" + # Create a set of plugins so duplicate plugins are not mistakenly added. + plugins = { + # Create a base64 representation of a picked plugin. + codecs.encode(pickle.dumps(p), "base64").decode() + # Merge current plugins with the new plugin. + for p in [*get_plugins(), plugin] + } + # Add plugins from cache. + CACHE.set("hyperglass.plugins.output", json.dumps(list(plugins))) + + +def register_output_plugin(plugin: OutputPlugin): + """Register an output plugin.""" + if issubclass(plugin, OutputPlugin): + plugin = plugin() + add_plugin(plugin) + log.info("Registered output plugin '{}'", plugin.__class__.__name__) diff --git a/hyperglass/plugins/main.py b/hyperglass/plugins/main.py new file mode 100644 index 0000000..59cf966 --- /dev/null +++ b/hyperglass/plugins/main.py @@ -0,0 +1,18 @@ +"""Register all plugins.""" + +# Standard Library +from inspect import isclass + +# Local +from . import _builtin +from ._output import OutputPlugin +from ._register import register_output_plugin + + +def init_plugins() -> None: + """Initialize all plugins.""" + for name in dir(_builtin): + plugin = getattr(_builtin, name) + if isclass(plugin): + if issubclass(plugin, OutputPlugin): + register_output_plugin(plugin)