From 7adb6ae0ece1b06612d1a50877351926a3015355 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Fri, 17 Sep 2021 09:04:59 -0700 Subject: [PATCH] Add `directives` to global state --- .flake8 | 1 + hyperglass/defaults/directives/__init__.py | 25 +++ hyperglass/defaults/directives/juniper.py | 165 ++++++++++++++++++ hyperglass/main.py | 3 + hyperglass/models/main.py | 13 +- .../plugins/_builtin/bgp_route_juniper.py | 6 +- hyperglass/state/hooks.py | 6 + hyperglass/state/manager.py | 6 - hyperglass/state/store.py | 22 +++ 9 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 hyperglass/defaults/directives/__init__.py create mode 100644 hyperglass/defaults/directives/juniper.py diff --git a/.flake8 b/.flake8 index 9eb9c50..0eb3eb8 100644 --- a/.flake8 +++ b/.flake8 @@ -12,6 +12,7 @@ per-file-ignores= hyperglass/models/api/*.py:N805,E0213,R0903,E501,C0301 hyperglass/models/commands/*.py:N805,E0213,R0903,E501,C0301 hyperglass/parsing/models/*.py:N805,E0213,R0903 + hyperglass/defaults/*/*.py:E501 hyperglass/configuration/models/*.py:N805,E0213,R0903,E501,C0301 # Disable unused import warning for modules hyperglass/*/__init__.py:F401 diff --git a/hyperglass/defaults/directives/__init__.py b/hyperglass/defaults/directives/__init__.py new file mode 100644 index 0000000..cb54314 --- /dev/null +++ b/hyperglass/defaults/directives/__init__.py @@ -0,0 +1,25 @@ +"""Built-in hyperglass directives.""" + +# Standard Library +import pkgutil +import importlib +from pathlib import Path + +# Project +from hyperglass.log import log +from hyperglass.state import use_state + + +def register_builtin_directives() -> None: + """Find all directives and register them with global state manager.""" + directives_dir = Path(__file__).parent + state = use_state() + for _, name, __ in pkgutil.iter_modules([directives_dir]): + module = importlib.import_module(f"hyperglass.defaults.directives.{name}") + + if not all((hasattr(module, "__all__"), len(getattr(module, "__all__", ())) > 0)): + # Warn if there is no __all__ export or if it is empty. + log.warning("Module '{!s}' is missing an '__all__' export", module) + + exports = (getattr(module, p) for p in module.__all__ if hasattr(module, p)) + state.add_directive(*exports) diff --git a/hyperglass/defaults/directives/juniper.py b/hyperglass/defaults/directives/juniper.py new file mode 100644 index 0000000..d53f524 --- /dev/null +++ b/hyperglass/defaults/directives/juniper.py @@ -0,0 +1,165 @@ +"""Default Juniper Directives.""" + +# Project +from hyperglass.models.directive import Rule, Text, NativeDirective + +__all__ = ( + "JuniperBGPRoute", + "JuniperBGPASPath", + "JuniperBGPCommunity", + "JuniperPing", + "JuniperTraceroute", + "JuniperBGPRouteTable", + "JuniperBGPASPathTable", + "JuniperBGPCommunityTable", +) + +JuniperBGPRoute = NativeDirective( + id="__hyperglass_juniper_bgp_route__", + name="BGP Route", + rules=[ + Rule( + condition="0.0.0.0/0", + action="permit", + command="show route protocol table inet.0 {target} detail", + ), + Rule( + condition="::/0", + action="permit", + command="show route protocol table inet6.0 {target} detail", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=["juniper"], +) + +JuniperBGPASPath = NativeDirective( + id="__hyperglass_juniper_bgp_aspath__", + name="BGP AS Path", + rules=[ + Rule( + condition="*", + action="permit", + commands=[ + 'show route protocol table inet.0 aspath-regex "{target}"', + 'show route protocol table inet6.0 aspath-regex "{target}"', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=["juniper"], +) + +JuniperBGPCommunity = NativeDirective( + id="__hyperglass_juniper_bgp_community__", + name="BGP Community", + rules=[ + Rule( + condition="*", + action="permit", + commands=[ + 'show route protocol table inet.0 community "{target}" detail', + 'show route protocol table inet6.0 community "{target}" detail', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + platforms=["juniper"], +) + + +JuniperPing = NativeDirective( + id="__hyperglass_juniper_ping__", + name="Ping", + rules=[ + Rule( + condition="0.0.0.0/0", + action="permit", + command="ping inet {target} count 5 source {source4}", + ), + Rule( + condition="::/0", + action="permit", + command="ping inet6 {target} count 5 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=["juniper"], +) + +JuniperTraceroute = NativeDirective( + id="__hyperglass_juniper_traceroute__", + name="Traceroute", + rules=[ + Rule( + condition="0.0.0.0/0", + action="permit", + command="traceroute inet {target} wait 1 source {source4}", + ), + Rule( + condition="::/0", + action="permit", + command="traceroute inet6 {target} wait 1 source {source6}", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=["juniper"], +) + +# Table Output Directives + +JuniperBGPRouteTable = NativeDirective( + id="__hyperglass_juniper_bgp_route_table__", + name="BGP Route", + rules=[ + Rule( + condition="0.0.0.0/0", + action="permit", + command="show route protocol bgp table inet.0 {target} best detail | display xml", + ), + Rule( + condition="::/0", + action="permit", + command="show route protocol bgp table inet6.0 {target} best detail | display xml", + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + table_output=True, + platforms=["juniper"], +) + +JuniperBGPASPathTable = NativeDirective( + id="__hyperglass_juniper_bgp_aspath_table__", + name="BGP AS Path", + rules=[ + Rule( + condition="*", + action="permit", + commands=[ + 'show route protocol bgp table inet.0 aspath-regex "{target}" detail | display xml', + 'show route protocol bgp table inet6.0 aspath-regex "{target}" detail | display xml', + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + table_output=True, + platforms=["juniper"], +) + +JuniperBGPCommunityTable = NativeDirective( + id="__hyperglass_juniper_bgp_community_table__", + name="BGP Community", + rules=[ + Rule( + condition="*", + action="permit", + commands=[ + "show route protocol bgp table inet.0 community {target} detail | display xml", + "show route protocol bgp table inet6.0 community {target} detail | display xml", + ], + ) + ], + field=Text(description="AS Path Regular Expression"), + table_output=True, + platforms=["juniper"], +) diff --git a/hyperglass/main.py b/hyperglass/main.py index 2556709..c409ebc 100644 --- a/hyperglass/main.py +++ b/hyperglass/main.py @@ -21,6 +21,7 @@ from .plugins import ( ) from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__ from .util.frontend import get_node_version +from .defaults.directives import register_builtin_directives if t.TYPE_CHECKING: # Local @@ -88,6 +89,8 @@ def on_starting(server: "Arbiter"): state = use_state() + register_builtin_directives() + register_all_plugins(state.devices) asyncio.run(build_ui()) diff --git a/hyperglass/models/main.py b/hyperglass/models/main.py index a80a585..c71d458 100644 --- a/hyperglass/models/main.py +++ b/hyperglass/models/main.py @@ -214,7 +214,16 @@ class HyperglassMultiModel(GenericModel, t.Generic[MultiModelT]): for o in (*self, *to_add) if getattr(o, unique_by) == v } - self.__root__ = list(unique_by_objects.values()) + new: t.List[MultiModelT] = list(unique_by_objects.values()) + else: - self.__root__ = [*self.__root__, *to_add] + new: t.List[MultiModelT] = [*self.__root__, *to_add] + self.__root__ = new self._count = len(self.__root__) + for item in new: + log.debug( + "Added {} '{!s}' to {}", + item.__class__.__name__, + getattr(item, self.accessor), + self.__class__.__name__, + ) diff --git a/hyperglass/plugins/_builtin/bgp_route_juniper.py b/hyperglass/plugins/_builtin/bgp_route_juniper.py index 369424a..92e8c78 100644 --- a/hyperglass/plugins/_builtin/bgp_route_juniper.py +++ b/hyperglass/plugins/_builtin/bgp_route_juniper.py @@ -122,9 +122,9 @@ class BGPRoutePluginJuniper(OutputPlugin): __hyperglass_builtin__: bool = PrivateAttr(True) platforms: Sequence[str] = ("juniper",) directives: Sequence[str] = ( - "__hyperglass_juniper_bgp_route__", - "__hyperglass_juniper_bgp_aspath__", - "__hyperglass_juniper_bgp_community__", + "__hyperglass_juniper_bgp_route_table__", + "__hyperglass_juniper_bgp_aspath_table__", + "__hyperglass_juniper_bgp_community_table__", ) def process(self, output: "OutputType", device: "Device") -> "OutputType": diff --git a/hyperglass/state/hooks.py b/hyperglass/state/hooks.py index 66058b9..4d6f8a3 100644 --- a/hyperglass/state/hooks.py +++ b/hyperglass/state/hooks.py @@ -14,6 +14,7 @@ from ..settings import Settings if t.TYPE_CHECKING: # Project from hyperglass.models.ui import UIParameters + from hyperglass.models.directive import Directives from hyperglass.models.config.params import Params from hyperglass.models.config.devices import Devices @@ -58,6 +59,11 @@ def use_state(attr: t.Literal["cache", "redis"]) -> "RedisManager": """Directly access hyperglass Redis cache manager.""" +@t.overload +def use_state(attr: t.Literal["directives"]) -> "Directives": + """Access all hyperglass directives.""" + + @t.overload def use_state(attr=None) -> "HyperglassState": """Access entire global state. diff --git a/hyperglass/state/manager.py b/hyperglass/state/manager.py index 66a4c0a..655b990 100644 --- a/hyperglass/state/manager.py +++ b/hyperglass/state/manager.py @@ -8,7 +8,6 @@ from redis import Redis, ConnectionPool # Project from hyperglass.util import repr_from_attrs -from hyperglass.configuration import params, devices, ui_params # Local from .redis import RedisManager @@ -36,11 +35,6 @@ class StateManager: redis = Redis(connection_pool=connection_pool) self.redis = RedisManager(instance=redis, namespace=self._namespace) - # Add configuration objects. - self.redis.set("params", params) - self.redis.set("devices", devices) - self.redis.set("ui_params", ui_params) - def __repr__(self) -> str: """Represent state manager by name and namespace.""" return repr_from_attrs(self, ("redis", "namespace")) diff --git a/hyperglass/state/store.py b/hyperglass/state/store.py index 21f730e..73fdc99 100644 --- a/hyperglass/state/store.py +++ b/hyperglass/state/store.py @@ -5,6 +5,9 @@ import codecs import pickle import typing as t +# Project +from hyperglass.configuration import params, devices, ui_params, directives + # Local from .manager import StateManager @@ -13,6 +16,7 @@ if t.TYPE_CHECKING: from hyperglass.models.ui import UIParameters from hyperglass.models.system import HyperglassSystem from hyperglass.plugins._base import HyperglassPlugin + from hyperglass.models.directive import Directive, Directives from hyperglass.models.config.params import Params from hyperglass.models.config.devices import Devices @@ -26,6 +30,13 @@ class HyperglassState(StateManager): def __init__(self, *, settings: "HyperglassSystem") -> None: """Initialize state store and reset plugins.""" super().__init__(settings=settings) + + # Add configuration objects. + self.redis.set("params", params) + self.redis.set("devices", devices) + self.redis.set("ui_params", ui_params) + self.redis.set("directives", directives) + # Ensure plugins are empty. self.reset_plugins("output") self.reset_plugins("input") @@ -57,6 +68,12 @@ class HyperglassState(StateManager): """Remove all plugins of `_type`.""" self.redis.set(("plugins", _type), []) + def add_directive(self, *directives: t.Union["Directive", t.Dict[str, t.Any]]) -> None: + """Add a directive.""" + current = self.directives + current.add(*directives, unique_by="id") + self.redis.set("directives", current) + def clear(self) -> None: """Delete all cache keys.""" self.redis.instance.flushdb(asynchronous=True) @@ -76,6 +93,11 @@ class HyperglassState(StateManager): """UI parameters, built from params.""" return self.redis.get("ui_params", raise_if_none=True) + @property + def directives(self) -> "Directives": + """All directives.""" + return self.redis.get("directives", raise_if_none=True) + def plugins(self, _type: str) -> t.List[PluginT]: """Get plugins by type.""" current = self.redis.get(("plugins", _type), raise_if_none=False, value_if_none=[])