From 2ba8b2c67c01006d2de26b9e225b2184e086e9e6 Mon Sep 17 00:00:00 2001 From: KyleM <103862795+ServerForge@users.noreply.github.com> Date: Fri, 6 Jun 2025 05:25:42 -0400 Subject: [PATCH 01/21] Fix VyOS Directives. (#284) * Fixed vyos directives syntax * Trying raw output * Switched from MTR to traceroute as hyperglass cannot handle the encoding of MTR output. --- hyperglass/defaults/directives/vyos.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hyperglass/defaults/directives/vyos.py b/hyperglass/defaults/directives/vyos.py index fcfb4ac..6aec00a 100644 --- a/hyperglass/defaults/directives/vyos.py +++ b/hyperglass/defaults/directives/vyos.py @@ -27,12 +27,12 @@ VyOS_BGPRoute = BuiltinDirective( RuleWithIPv4( condition="0.0.0.0/0", action="permit", - command="show ip bgp {target}", + command="show bgp ipv4 {target}", ), RuleWithIPv6( condition="::/0", action="permit", - command="show ipv6 bgp {target}", + command="show bgp ipv6 {target}", ), ], field=Text(description="IP Address, Prefix, or Hostname"), @@ -47,8 +47,8 @@ VyOS_BGPASPath = BuiltinDirective( condition="*", action="permit", commands=[ - 'show ip bgp regexp "{target}"', - 'show ipv6 bgp regexp "{target}"', + 'show bgp ipv4 regexp "{target}"', + 'show bgp ipv6 regexp "{target}"', ], ) ], @@ -64,8 +64,8 @@ VyOS_BGPCommunity = BuiltinDirective( condition="*", action="permit", commands=[ - "show ip bgp community {target}", - "show ipv6 bgp community {target}", + "show bgp ipv4 community {target}", + "show bgp ipv6 community {target}", ], ) ], @@ -99,12 +99,12 @@ VyOS_Traceroute = BuiltinDirective( RuleWithIPv4( condition="0.0.0.0/0", action="permit", - command="mtr -4 -G 1 -c 1 -w -o SAL -a {source4} {target}", + command="traceroute {target} source-address {source4} icmp", ), RuleWithIPv6( condition="::/0", action="permit", - command="mtr -6 -G 1 -c 1 -w -o SAL -a {source6} {target}", + command="traceroute {target} source-address {source6} icmp", ), ], field=Text(description="IP Address, Prefix, or Hostname"), From e6e11ee058dfc9e437ab557cd74ddb25cb1d773e Mon Sep 17 00:00:00 2001 From: GrandArcher Date: Fri, 6 Jun 2025 05:36:54 -0400 Subject: [PATCH 02/21] Update mikrotik.py (#292) Fixed Mikrotik_BGPRoute to print all routes that contain {target} instead of having to be an exact match to {target}. --- hyperglass/defaults/directives/mikrotik.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hyperglass/defaults/directives/mikrotik.py b/hyperglass/defaults/directives/mikrotik.py index 16c948a..4ea96fd 100644 --- a/hyperglass/defaults/directives/mikrotik.py +++ b/hyperglass/defaults/directives/mikrotik.py @@ -27,12 +27,12 @@ Mikrotik_BGPRoute = BuiltinDirective( RuleWithIPv4( condition="0.0.0.0/0", action="permit", - command="ip route print where dst-address={target}", + command="ip route print where {target} in dst-address", ), RuleWithIPv6( condition="::/0", action="permit", - command="ipv6 route print where dst-address={target}", + command="ipv6 route print where {target} in dst-address", ), ], field=Text(description="IP Address, Prefix, or Hostname"), From 7109ee6c97d89d5aac2151bd58927e3cfd5ffb39 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 6 Jun 2025 04:59:27 -0400 Subject: [PATCH 03/21] adds unreleased section --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b17f6cc..51a7d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- [#305](https://github.com/thatmattlove/hyperglass/issues/264): Fix: allow integer values in ext_community_list_raw field for Arista BGP - @cooperwinser + ## 2.0.4 - 2024-06-30 ### Fixed From 4106a10dbc99609b3ed71e5cee8633733ebcacf1 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 6 Jun 2025 05:40:54 -0400 Subject: [PATCH 04/21] PRs 284,292,306 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a7d2d..3323d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed - [#305](https://github.com/thatmattlove/hyperglass/issues/264): Fix: allow integer values in ext_community_list_raw field for Arista BGP - @cooperwinser +- [#245](https://github.com/thatmattlove/hyperglass/issues/245): v2.0.0 Vyos version platforms - Moved to latest LTS command set. - @ServerForge + +### Updated + +- [#292](https://github.com/thatmattlove/hyperglass/pull/292): Updates Mikrotik BGP route command so supernets are selected as well as exact matches. + ## 2.0.4 - 2024-06-30 From 91e375693a49829761de10b7342618853a0e4c53 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 6 Jun 2025 06:01:48 -0400 Subject: [PATCH 05/21] Adds system package install --- .github/workflows/backend.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index edc2c0a..a45550a 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -23,6 +23,11 @@ jobs: - name: Git Checkout uses: actions/checkout@v3 + - name: Install system pachages + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev pkg-config python3-dev + - name: Install Python uses: actions/setup-python@v5 with: @@ -48,7 +53,6 @@ jobs: - name: Prepare run: | - sudo apt-get install libcairo2-dev -y mkdir -p "$HOME/hyperglass" echo "HYPERGLASS_APP_PATH=$HOME/hyperglass" >> $GITHUB_ENV echo "HYPERGLASS_HOST=127.0.0.1" >> $GITHUB_ENV From 8fbf25dd14ca0cb58cce487360ea9b276df52362 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 6 Jun 2025 06:16:23 -0400 Subject: [PATCH 06/21] Adds optional step to switch to a release tag --- docs/pages/installation/manual.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/pages/installation/manual.mdx b/docs/pages/installation/manual.mdx index ef17b03..189d7f4 100644 --- a/docs/pages/installation/manual.mdx +++ b/docs/pages/installation/manual.mdx @@ -25,6 +25,8 @@ Once these dependencies are installed, install hyperglass via PyPI: ```shell copy git clone https://github.com/thatmattlove/hyperglass --depth=1 cd hyperglass +# optional - switch to the latest stable release +# git switch -c v2.0.4 v2.0.4 pip3 install -e . ``` From 7ae40e8cc8108b5bf7d40117e06c806cacb92773 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sun, 8 Jun 2025 21:33:31 -0400 Subject: [PATCH 07/21] updates frontend workflows --- .github/workflows/frontend.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 55bf46f..e97859f 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -12,8 +12,8 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x] - pnpm-version: [8] + node-version: [20.x, 21.x, 22.x] + pnpm-version: [9, 10] os: [ubuntu-latest] runs-on: ${{ matrix.os }} env: From 51c7f9eef6ea8d70178716edb34fea5bafac572f Mon Sep 17 00:00:00 2001 From: Chris Wiggins Date: Sun, 24 Nov 2024 21:40:02 +0800 Subject: [PATCH 08/21] feat: Add FRR structured output for BGP Routes --- .samples/frr_bgp_route.json | 13 +-- hyperglass/constants.py | 2 +- hyperglass/defaults/directives/frr.py | 23 +++++ hyperglass/models/parsing/frr.py | 23 ++--- hyperglass/plugins/_builtin/__init__.py | 2 + hyperglass/plugins/_builtin/bgp_route_frr.py | 86 +++++++++++++++++++ .../plugins/tests/test_bgp_route_frr.py | 55 ++++++++++++ 7 files changed, 184 insertions(+), 20 deletions(-) create mode 100644 hyperglass/plugins/_builtin/bgp_route_frr.py create mode 100644 hyperglass/plugins/tests/test_bgp_route_frr.py diff --git a/.samples/frr_bgp_route.json b/.samples/frr_bgp_route.json index 90b936d..aae884c 100644 --- a/.samples/frr_bgp_route.json +++ b/.samples/frr_bgp_route.json @@ -12,13 +12,8 @@ ], "length": 2 }, - "aggregatorAs": 13335, - "aggregatorId": "108.162.239.1", "origin": "IGP", - "med": 25090, - "metric": 25090, - "localpref": 100, - "weight": 100, + "locPrf": 100, "valid": true, "community": { "string": "174:21001 174:22003 14525:0 14525:40 14525:1021 14525:2840 14525:3003 14525:4004 14525:9001", @@ -70,7 +65,7 @@ "origin": "IGP", "med": 0, "metric": 0, - "localpref": 150, + "locPrf": 150, "weight": 200, "valid": true, "bestpath": { @@ -124,7 +119,7 @@ "origin": "IGP", "med": 0, "metric": 0, - "localpref": 100, + "locPrf": 100, "weight": 100, "valid": true, "bestpath": { @@ -180,7 +175,7 @@ "origin": "IGP", "med": 2020, "metric": 2020, - "localpref": 150, + "locPrf": 150, "weight": 200, "valid": true, "bestpath": { diff --git a/hyperglass/constants.py b/hyperglass/constants.py index 5fb0870..0139c74 100644 --- a/hyperglass/constants.py +++ b/hyperglass/constants.py @@ -19,7 +19,7 @@ TARGET_FORMAT_SPACE = ("huawei", "huawei_vrpv8") TARGET_JUNIPER_ASPATH = ("juniper", "juniper_junos") -SUPPORTED_STRUCTURED_OUTPUT = ("juniper", "arista_eos") +SUPPORTED_STRUCTURED_OUTPUT = ("frr", "juniper", "arista_eos") CONFIG_EXTENSIONS = ("py", "yaml", "yml", "json", "toml") diff --git a/hyperglass/defaults/directives/frr.py b/hyperglass/defaults/directives/frr.py index 09e8f2d..eb6baaf 100644 --- a/hyperglass/defaults/directives/frr.py +++ b/hyperglass/defaults/directives/frr.py @@ -15,6 +15,7 @@ __all__ = ( "FRRouting_BGPRoute", "FRRouting_Ping", "FRRouting_Traceroute", + "FRRouting_BGPRouteTable", ) NAME = "FRRouting" @@ -36,6 +37,7 @@ FRRouting_BGPRoute = BuiltinDirective( ), ], field=Text(description="IP Address, Prefix, or Hostname"), + table_output="__hyperglass_frr_bgp_route_table__", platforms=PLATFORMS, ) @@ -110,3 +112,24 @@ FRRouting_Traceroute = BuiltinDirective( field=Text(description="IP Address, Prefix, or Hostname"), platforms=PLATFORMS, ) + +# Table Output Directives + +FRRouting_BGPRouteTable = BuiltinDirective( + id="__hyperglass_frr_bgp_route_table__", + name="BGP Route", + rules=[ + RuleWithIPv4( + condition="0.0.0.0/0", + action="permit", + command='vtysh -c "show bgp ipv4 unicast {target} json"', + ), + RuleWithIPv6( + condition="::/0", + action="permit", + command='vtysh -c "show bgp ipv6 unicast {target} json"', + ), + ], + field=Text(description="IP Address, Prefix, or Hostname"), + platforms=PLATFORMS, +) diff --git a/hyperglass/models/parsing/frr.py b/hyperglass/models/parsing/frr.py index 895046b..2dbac8a 100644 --- a/hyperglass/models/parsing/frr.py +++ b/hyperglass/models/parsing/frr.py @@ -14,7 +14,7 @@ from hyperglass.models.data import BGPRouteTable # Local from ..main import HyperglassModel -FRRPeerType = t.Literal["internal", "external"] +FRRPeerType = t.Literal["internal", "external", "confed-internal", "confed-external"] def _alias_generator(field): @@ -48,11 +48,12 @@ class FRRPath(_FRRBase): """FRR Path Model.""" aspath: t.List[int] - aggregator_as: int - aggregator_id: str + aggregator_as: int = 0 + aggregator_id: str = "" + loc_prf: int = 100 # 100 is the default value for local preference + metric: int = 0 med: int = 0 - localpref: int - weight: int + weight: int = 0 valid: bool last_update: int bestpath: bool @@ -60,25 +61,26 @@ class FRRPath(_FRRBase): nexthops: t.List[FRRNextHop] peer: FRRPeer - @model_validator(pre=True) + @model_validator(mode="before") def validate_path(cls, values): """Extract meaningful data from FRR response.""" new = values.copy() new["aspath"] = values["aspath"]["segments"][0]["list"] - new["community"] = values["community"]["list"] + community = values.get("community", {'list': []}) + new["community"] = community["list"] new["lastUpdate"] = values["lastUpdate"]["epoch"] bestpath = values.get("bestpath", {}) new["bestpath"] = bestpath.get("overall", False) return new -class FRRRoute(_FRRBase): +class FRRBGPTable(_FRRBase): """FRR Route Model.""" prefix: str paths: t.List[FRRPath] = [] - def serialize(self): + def bgp_table(self): """Convert the FRR-specific fields to standard parsed data model.""" # TODO: somehow, get the actual VRF @@ -96,7 +98,7 @@ class FRRRoute(_FRRBase): "age": age, "weight": route.weight, "med": route.med, - "local_preference": route.localpref, + "local_preference": route.loc_prf, "as_path": route.aspath, "communities": route.community, "next_hop": route.nexthops[0].ip, @@ -104,6 +106,7 @@ class FRRRoute(_FRRBase): "source_rid": route.aggregator_id, "peer_rid": route.peer.peer_id, # TODO: somehow, get the actual RPKI state + # This depends on whether or not the RPKI module is enabled in FRR "rpki_state": 3, } ) diff --git a/hyperglass/plugins/_builtin/__init__.py b/hyperglass/plugins/_builtin/__init__.py index 4a36861..cefb627 100644 --- a/hyperglass/plugins/_builtin/__init__.py +++ b/hyperglass/plugins/_builtin/__init__.py @@ -3,11 +3,13 @@ # Local from .remove_command import RemoveCommand from .bgp_route_arista import BGPRoutePluginArista +from .bgp_route_frr import BGPRoutePluginFrr from .bgp_route_juniper import BGPRoutePluginJuniper from .mikrotik_garbage_output import MikrotikGarbageOutput __all__ = ( "BGPRoutePluginArista", + "BGPRoutePluginFrr", "BGPRoutePluginJuniper", "MikrotikGarbageOutput", "RemoveCommand", diff --git a/hyperglass/plugins/_builtin/bgp_route_frr.py b/hyperglass/plugins/_builtin/bgp_route_frr.py new file mode 100644 index 0000000..627fad2 --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_route_frr.py @@ -0,0 +1,86 @@ +"""Parse FRR JSON Response to Structured Data.""" + +# Standard Library +import json +import typing as t + +# Third Party +from pydantic import PrivateAttr, ValidationError + +# Project +from hyperglass.log import log +from hyperglass.exceptions.private import ParsingError +from hyperglass.models.parsing.frr import FRRBGPTable + +# Local +from .._output import OutputPlugin + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.data import OutputDataModel + from hyperglass.models.api.query import Query + + # Local + from .._output import OutputType + + +def parse_frr(output: t.Sequence[str]) -> "OutputDataModel": + """Parse a FRR BGP JSON response.""" + result = None + + _log = log.bind(plugin=BGPRoutePluginFrr.__name__) + + for response in output: + try: + parsed: t.Dict = json.loads(response) + + _log.debug("Pre-parsed data", data=parsed) + + validated = FRRBGPTable(**parsed) + bgp_table = validated.bgp_table() + + if result is None: + result = bgp_table + else: + result += bgp_table + + except json.JSONDecodeError as err: + _log.bind(error=str(err)).critical("Failed to decode JSON") + raise ParsingError("Error parsing response data") from err + + except KeyError as err: + _log.bind(key=str(err)).critical("Missing required key in response") + raise ParsingError("Error parsing response data") from err + + except IndexError as err: + _log.critical(err) + raise ParsingError("Error parsing response data") from err + + except ValidationError as err: + _log.critical(err) + raise ParsingError(err.errors()) from err + + return result + +class BGPRoutePluginFrr(OutputPlugin): + """Coerce a FRR route table in JSON format to a standard BGP Table structure.""" + + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: t.Sequence[str] = ("frr",) + directives: t.Sequence[str] = ( + "__hyperglass_frr_bgp_route_table__", + ) + + def process(self, *, output: "OutputType", query: "Query") -> "OutputType": + """Parse FRR response if data is a string (and is therefore unparsed).""" + should_process = all( + ( + isinstance(output, (list, tuple)), + query.device.platform in self.platforms, + query.device.structured_output is True, + query.device.has_directives(*self.directives), + ) + ) + if should_process: + return parse_frr(output) + return output diff --git a/hyperglass/plugins/tests/test_bgp_route_frr.py b/hyperglass/plugins/tests/test_bgp_route_frr.py new file mode 100644 index 0000000..bd042b2 --- /dev/null +++ b/hyperglass/plugins/tests/test_bgp_route_frr.py @@ -0,0 +1,55 @@ +"""FRR BGP Route Parsing Tests.""" + +# flake8: noqa +# Standard Library +from pathlib import Path + +# Third Party +import pytest + +# Project +from hyperglass.models.config.devices import Device +from hyperglass.models.data.bgp_route import BGPRouteTable + +# Local +from ._fixtures import MockDevice +from .._builtin.bgp_route_frr import BGPRoutePluginFrr + +DEPENDS_KWARGS = { + "depends": [ + "hyperglass/models/tests/test_util.py::test_check_legacy_fields", + "hyperglass/external/tests/test_rpki.py::test_rpki", + ], + "scope": "session", +} + +SAMPLE = Path(__file__).parent.parent.parent.parent / ".samples" / "frr_bgp_route.json" + + +def _tester(sample: str): + plugin = BGPRoutePluginFrr() + + device = MockDevice( + name="Test Device", + address="127.0.0.1", + group="Test Network", + credential={"username": "", "password": ""}, + platform="frr", + structured_output=True, + directives=["__hyperglass_frr_bgp_route_table__"], + attrs={"source4": "192.0.2.1", "source6": "2001:db8::1"}, + ) + + query = type("Query", (), {"device": device}) + + result = plugin.process(output=(sample,), query=query) + assert isinstance(result, BGPRouteTable), "Invalid parsed result" + assert hasattr(result, "count"), "BGP Table missing count" + assert result.count > 0, "BGP Table count is 0" + + +@pytest.mark.dependency(**DEPENDS_KWARGS) +def test_frr_route_sample(): + with SAMPLE.open("r") as file: + sample = file.read() + return _tester(sample) From b7abe8d027751d2717b93df7d1e69e41e8203c56 Mon Sep 17 00:00:00 2001 From: Daniel Matsson Date: Sat, 17 May 2025 15:51:59 +0200 Subject: [PATCH 09/21] Fix code block padding in docs --- docs/pages/plugins.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages/plugins.mdx b/docs/pages/plugins.mdx index 2f256f8..adc5087 100644 --- a/docs/pages/plugins.mdx +++ b/docs/pages/plugins.mdx @@ -60,13 +60,13 @@ ip_route_directive: When the query is received, the query target is transformed, resulting in this being sent to the device: -``` +```text show ip route 192.0.2.0 255.255.255.0 ``` instead of: -``` +```text show ip route 192.0.2.0/24 ``` From 44f8faa1e576b534e0aaba0469ceb194d8d371de Mon Sep 17 00:00:00 2001 From: Jelson Stoelben Rodrigues Date: Mon, 26 May 2025 20:11:19 -0300 Subject: [PATCH 10/21] fix: timeout when fetching bgp routes Huawei has pagination by default and when the output exceeds 24 lines it expects the user to keep scrooling to display the output. This makes the netmiko time out. By passing "| no-more" the pagination will be disabled for the command --- hyperglass/defaults/directives/huawei.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hyperglass/defaults/directives/huawei.py b/hyperglass/defaults/directives/huawei.py index b517503..f8d3ef0 100644 --- a/hyperglass/defaults/directives/huawei.py +++ b/hyperglass/defaults/directives/huawei.py @@ -27,15 +27,16 @@ Huawei_BGPRoute = BuiltinDirective( RuleWithIPv4( condition="0.0.0.0/0", action="permit", - command="display bgp routing-table {target}", + command="display bgp routing-table {target} | no-more", ), RuleWithIPv6( condition="::/0", action="permit", - command="display bgp ipv6 routing-table {target}", + command="display bgp ipv6 routing-table {target} | no-more", ), ], field=Text(description="IP Address, Prefix, or Hostname"), + plugins=["bgp_route_huawei"], platforms=PLATFORMS, ) From f67c676a2dd58c8bd62845b81b58fb0bf91e71c8 Mon Sep 17 00:00:00 2001 From: Jelson Stoelben Rodrigues Date: Mon, 26 May 2025 20:14:36 -0300 Subject: [PATCH 11/21] feat: add Huawei BGP Route Input Plugin Create builtin plugin to transform input field before passing to Huawei device --- hyperglass/plugins/_builtin/__init__.py | 2 + .../plugins/_builtin/bgp_route_huawei.py | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 hyperglass/plugins/_builtin/bgp_route_huawei.py diff --git a/hyperglass/plugins/_builtin/__init__.py b/hyperglass/plugins/_builtin/__init__.py index cefb627..8f8bd0f 100644 --- a/hyperglass/plugins/_builtin/__init__.py +++ b/hyperglass/plugins/_builtin/__init__.py @@ -6,11 +6,13 @@ from .bgp_route_arista import BGPRoutePluginArista from .bgp_route_frr import BGPRoutePluginFrr from .bgp_route_juniper import BGPRoutePluginJuniper from .mikrotik_garbage_output import MikrotikGarbageOutput +from .bgp_route_huawei import BGPRoutePluginHuawei __all__ = ( "BGPRoutePluginArista", "BGPRoutePluginFrr", "BGPRoutePluginJuniper", + "BGPRoutePluginHuawei", "MikrotikGarbageOutput", "RemoveCommand", ) diff --git a/hyperglass/plugins/_builtin/bgp_route_huawei.py b/hyperglass/plugins/_builtin/bgp_route_huawei.py new file mode 100644 index 0000000..86e70a3 --- /dev/null +++ b/hyperglass/plugins/_builtin/bgp_route_huawei.py @@ -0,0 +1,42 @@ +from ipaddress import ip_network +from .._input import InputPlugin + +# Standard Library +import typing as t + +# Third Party +from pydantic import PrivateAttr + +if t.TYPE_CHECKING: + # Project + from hyperglass.models.api.query import Query + +InputPluginTransformReturn = t.Union[t.Sequence[str], str] + +class BGPRoutePluginHuawei(InputPlugin): + _hyperglass_builtin: bool = PrivateAttr(True) + platforms: t.Sequence[str] =("huawei", "huawei_vrpv8",) + directives: t.Sequence[str] = ("__hyperglass_huawei_bgp_route__",) + + """ + Huawei BGP Route Input Plugin + + This plugin transforms a query target into a network address and prefix length + ex.: 192.0.2.0/24 -> 192.0.2.0 24 + ex.: 2001:db8::/32 -> 2001:db8:: 32 + """ + def transform(self, query: "Query") -> InputPluginTransformReturn: + (target := query.query_target) + + if not target or not isinstance(target, list) or len(target) == 0: + return None + + target = target[0].strip() + + # Check for the / in the query target + if target.find("/") == -1: + return target + + target_network = ip_network(target) + + return f"{target_network.network_address!s} {target_network.prefixlen!s}" \ No newline at end of file From 23522da75432d85602d8408bf29f7a050fb7b67e Mon Sep 17 00:00:00 2001 From: Jelson Stoelben Rodrigues Date: Mon, 26 May 2025 20:15:21 -0300 Subject: [PATCH 12/21] fix: ensure query_target is transformed after validation Transformation must occur after validation, or else the transformation will possibly make the validation fail --- hyperglass/models/api/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hyperglass/models/api/query.py b/hyperglass/models/api/query.py index 2f2a1c7..571311f 100644 --- a/hyperglass/models/api/query.py +++ b/hyperglass/models/api/query.py @@ -65,12 +65,12 @@ class Query(BaseModel): self._input_plugin_manager = InputPluginManager() - self.query_target = self.transform_query_target() - try: self.validate_query_target() except InputValidationError as err: raise InputInvalid(**err.kwargs) from err + + self.query_target = self.transform_query_target() def summary(self) -> SimpleQuery: """Summarized and post-validated model of a Query.""" From 7b06930e1b7a5c6dff5497c608572f91563b5fe6 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Mon, 9 Jun 2025 21:41:11 -0400 Subject: [PATCH 13/21] Adding unrelease changes [skip ci] --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3323d21..9baaa6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed -- [#305](https://github.com/thatmattlove/hyperglass/issues/264): Fix: allow integer values in ext_community_list_raw field for Arista BGP - @cooperwinser -- [#245](https://github.com/thatmattlove/hyperglass/issues/245): v2.0.0 Vyos version platforms - Moved to latest LTS command set. - @ServerForge +- [#306](https://github.com/thatmattlove/hyperglass/issues/306): Fix: allow integer values in ext_community_list_raw field for Arista BGP - @cooperwinser +- [#325](https://github.com/thatmattlove/hyperglass/pull/325): Fix code block padding in the documentation - @jagardaniel +- [#327](https://github.com/thatmattlove/hyperglass/pull/327): Fix huawei bgp route and plugin validation/transform order - @JelsonRodrigues ### Updated -- [#292](https://github.com/thatmattlove/hyperglass/pull/292): Updates Mikrotik BGP route command so supernets are selected as well as exact matches. +- [#245](https://github.com/thatmattlove/hyperglass/issues/245): v2.0.0 Vyos version platforms - Moved to latest LTS command set. - @ServerForge +- [#292](https://github.com/thatmattlove/hyperglass/pull/292): Updates Mikrotik BGP route command so supernets are selected as well as exact matches. - @GrandArcher +### Added + +- [#304](https://github.com/thatmattlove/hyperglass/pull/304): Add FRR structured output for BGP Routes - @chriswiggins ## 2.0.4 - 2024-06-30 From cf137bd7e8581f707ab521599d4ed95e9b84e5a7 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sat, 14 Jun 2025 21:44:20 -0400 Subject: [PATCH 14/21] format and lint --- hyperglass/api/__init__.py | 1 + hyperglass/cli/echo.py | 1 + hyperglass/cli/main.py | 11 ++++---- hyperglass/exceptions/_common.py | 4 +-- hyperglass/execution/drivers/_construct.py | 4 +-- hyperglass/execution/drivers/ssh.py | 6 ++--- hyperglass/external/_base.py | 6 ++--- hyperglass/external/bgptools.py | 4 +-- hyperglass/external/tests/test_base.py | 1 + hyperglass/external/tests/test_bgptools.py | 1 - hyperglass/external/tests/test_rpki.py | 9 ++++--- hyperglass/models/api/__init__.py | 1 + hyperglass/models/api/query.py | 2 +- hyperglass/models/config/cache.py | 1 - hyperglass/models/data/bgp_route.py | 2 +- hyperglass/models/parsing/frr.py | 4 +-- hyperglass/models/tests/test_util.py | 6 ++--- hyperglass/plugins/_builtin/__init__.py | 4 +-- hyperglass/plugins/_builtin/bgp_route_frr.py | 5 ++-- .../plugins/_builtin/bgp_route_huawei.py | 27 +++++++++++-------- .../plugins/tests/test_bgp_community.py | 1 + hyperglass/state/tests/test_hooks.py | 6 ++--- hyperglass/ui/pnpm-workspace.yaml | 3 +++ 23 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 hyperglass/ui/pnpm-workspace.yaml diff --git a/hyperglass/api/__init__.py b/hyperglass/api/__init__.py index 5684629..65458f9 100644 --- a/hyperglass/api/__init__.py +++ b/hyperglass/api/__init__.py @@ -1,4 +1,5 @@ """hyperglass API.""" + # Standard Library import logging diff --git a/hyperglass/cli/echo.py b/hyperglass/cli/echo.py index 642c1ce..4f7ced0 100644 --- a/hyperglass/cli/echo.py +++ b/hyperglass/cli/echo.py @@ -1,4 +1,5 @@ """Helper functions for CLI message printing.""" + # Standard Library import typing as t diff --git a/hyperglass/cli/main.py b/hyperglass/cli/main.py index 2ee2107..7cc06f6 100644 --- a/hyperglass/cli/main.py +++ b/hyperglass/cli/main.py @@ -33,7 +33,7 @@ def run(): def _version( version: t.Optional[bool] = typer.Option( None, "--version", help="hyperglass version", callback=_version - ) + ), ) -> None: """hyperglass""" pass @@ -77,7 +77,6 @@ def _build_ui(timeout: int = typer.Option(180, help="Timeout in seconds")) -> No with echo._console.status( f"Starting new UI build with a {timeout} second timeout...", spinner="aesthetic" ): - _build_ui(timeout=120) @@ -140,7 +139,7 @@ def _clear_cache(): @cli.command(name="devices") def _devices( - search: t.Optional[str] = typer.Argument(None, help="Device ID or Name Search Pattern") + search: t.Optional[str] = typer.Argument(None, help="Device ID or Name Search Pattern"), ): """Show all configured devices""" # Third Party @@ -189,7 +188,7 @@ def _devices( @cli.command(name="directives") def _directives( - search: t.Optional[str] = typer.Argument(None, help="Directive ID or Name Search Pattern") + search: t.Optional[str] = typer.Argument(None, help="Directive ID or Name Search Pattern"), ): """Show all configured devices""" # Third Party @@ -280,7 +279,7 @@ def _plugins( def _params( path: t.Optional[str] = typer.Argument( None, help="Parameter Object Path, for example 'messages.no_input'" - ) + ), ): """Show configuration parameters""" # Standard Library @@ -312,7 +311,7 @@ def _params( ) raise typer.Exit(0) except AttributeError: - echo.error(f"{'params.'+path!r} does not exist") + echo.error(f"{'params.' + path!r} does not exist") raise typer.Exit(1) panel = Inspect( diff --git a/hyperglass/exceptions/_common.py b/hyperglass/exceptions/_common.py index 9963324..1f04f94 100644 --- a/hyperglass/exceptions/_common.py +++ b/hyperglass/exceptions/_common.py @@ -2,7 +2,7 @@ # Standard Library import json as _json -from typing import Any, Dict, List, Union, Literal, Optional, Set +from typing import Any, Set, Dict, List, Union, Literal, Optional # Third Party from pydantic import ValidationError @@ -72,7 +72,7 @@ class HyperglassError(Exception): for err in errors: loc = " → ".join(str(loc) for loc in err["loc"]) - errs += (f'Field: {loc}\n Error: {err["msg"]}\n',) + errs += (f"Field: {loc}\n Error: {err['msg']}\n",) return "\n".join(errs) diff --git a/hyperglass/execution/drivers/_construct.py b/hyperglass/execution/drivers/_construct.py index 86032bc..c1d1e2b 100644 --- a/hyperglass/execution/drivers/_construct.py +++ b/hyperglass/execution/drivers/_construct.py @@ -94,7 +94,7 @@ class Construct: for key in [k for k in keys if k != "target" and k != "mask"]: if key not in attrs: raise ConfigError( - ("Command '{c}' has attribute '{k}', " "which is missing from device '{d}'"), + ("Command '{c}' has attribute '{k}', which is missing from device '{d}'"), level="danger", c=self.directive.name, k=key, @@ -224,4 +224,4 @@ class Formatter: def _bird_bgp_community(self, target: str) -> str: """Convert from standard community format to BIRD format.""" parts = target.split(":") - return f'({",".join(parts)})' + return f"({','.join(parts)})" diff --git a/hyperglass/execution/drivers/ssh.py b/hyperglass/execution/drivers/ssh.py index 13e070e..25d81b4 100644 --- a/hyperglass/execution/drivers/ssh.py +++ b/hyperglass/execution/drivers/ssh.py @@ -44,9 +44,9 @@ class SSHConnection(Connection): if proxy.credential._method == "encrypted_key": # If the key is encrypted, use the password field as the # private key password. - tunnel_kwargs[ - "ssh_private_key_password" - ] = proxy.credential.password.get_secret_value() + tunnel_kwargs["ssh_private_key_password"] = ( + proxy.credential.password.get_secret_value() + ) try: return open_tunnel(proxy._target, proxy.port, **tunnel_kwargs) diff --git a/hyperglass/external/_base.py b/hyperglass/external/_base.py index 60c35da..3954c51 100644 --- a/hyperglass/external/_base.py +++ b/hyperglass/external/_base.py @@ -212,7 +212,7 @@ class BaseExternal: if method.upper() not in supported_methods: raise self._exception( - f'Method must be one of {", ".join(supported_methods)}. ' f"Got: {str(method)}" + f"Method must be one of {', '.join(supported_methods)}. Got: {str(method)}" ) endpoint = "/".join( @@ -284,7 +284,7 @@ class BaseExternal: status = httpx.codes(response.status_code) error = self._parse_response(response) raise self._exception( - f'{status.name.replace("_", " ")}: {error}', level="danger" + f"{status.name.replace('_', ' ')}: {error}", level="danger" ) from None except httpx.HTTPError as http_err: @@ -340,7 +340,7 @@ class BaseExternal: status = httpx.codes(response.status_code) error = self._parse_response(response) raise self._exception( - f'{status.name.replace("_", " ")}: {error}', level="danger" + f"{status.name.replace('_', ' ')}: {error}", level="danger" ) from None except httpx.HTTPError as http_err: diff --git a/hyperglass/external/bgptools.py b/hyperglass/external/bgptools.py index 1ea1cee..f2fd7f6 100644 --- a/hyperglass/external/bgptools.py +++ b/hyperglass/external/bgptools.py @@ -35,7 +35,7 @@ def default_ip_targets(*targets: str) -> t.Tuple[TargetData, t.Tuple[str, ...]]: default_data = {} query = () for target in targets: - detail: TargetDetail = {k: "None" for k in DEFAULT_KEYS} + detail: TargetDetail = dict.fromkeys(DEFAULT_KEYS, "None") try: valid: t.Union[IPv4Address, IPv6Address] = ip_address(target) @@ -139,7 +139,7 @@ async def network_info(*targets: str) -> TargetData: cache = use_state("cache") # Set default data structure. - query_data = {t: {k: "" for k in DEFAULT_KEYS} for t in query_targets} + query_data = {t: dict.fromkeys(DEFAULT_KEYS, "") for t in query_targets} # Get all cached bgp.tools data. cached = cache.get_map(CACHE_KEY) or {} diff --git a/hyperglass/external/tests/test_base.py b/hyperglass/external/tests/test_base.py index 5f23594..6604f4e 100644 --- a/hyperglass/external/tests/test_base.py +++ b/hyperglass/external/tests/test_base.py @@ -1,4 +1,5 @@ """Test external http client.""" + # Standard Library import asyncio diff --git a/hyperglass/external/tests/test_bgptools.py b/hyperglass/external/tests/test_bgptools.py index ca8f98d..542c1dc 100644 --- a/hyperglass/external/tests/test_bgptools.py +++ b/hyperglass/external/tests/test_bgptools.py @@ -16,7 +16,6 @@ WHOIS_OUTPUT = """AS | IP | BGP Prefix | CC | Registry | Allocated | AS # Ignore asyncio deprecation warning about loop @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_network_info(): - checks = ( ("192.0.2.1", {"asn": "None", "rir": "Private Address"}), ("127.0.0.1", {"asn": "None", "rir": "Loopback Address"}), diff --git a/hyperglass/external/tests/test_rpki.py b/hyperglass/external/tests/test_rpki.py index 66da2e4..01438e7 100644 --- a/hyperglass/external/tests/test_rpki.py +++ b/hyperglass/external/tests/test_rpki.py @@ -1,4 +1,5 @@ """Test RPKI data fetching.""" + # Third Party import pytest @@ -18,8 +19,8 @@ def test_rpki(): result = rpki_state(prefix, asn) result_name = RPKI_NAME_MAP.get(result, "No Name") expected_name = RPKI_NAME_MAP.get(expected, "No Name") - assert ( - result == expected - ), "RPKI State for '{}' via AS{!s} '{}' ({}) instead of '{}' ({})".format( - prefix, asn, result, result_name, expected, expected_name + assert result == expected, ( + "RPKI State for '{}' via AS{!s} '{}' ({}) instead of '{}' ({})".format( + prefix, asn, result, result_name, expected, expected_name + ) ) diff --git a/hyperglass/models/api/__init__.py b/hyperglass/models/api/__init__.py index 70769f4..8f4bfcd 100644 --- a/hyperglass/models/api/__init__.py +++ b/hyperglass/models/api/__init__.py @@ -1,4 +1,5 @@ """Query & Response Validation Models.""" + # Local from .query import Query from .response import ( diff --git a/hyperglass/models/api/query.py b/hyperglass/models/api/query.py index 571311f..3b941fa 100644 --- a/hyperglass/models/api/query.py +++ b/hyperglass/models/api/query.py @@ -69,7 +69,7 @@ class Query(BaseModel): self.validate_query_target() except InputValidationError as err: raise InputInvalid(**err.kwargs) from err - + self.query_target = self.transform_query_target() def summary(self) -> SimpleQuery: diff --git a/hyperglass/models/config/cache.py b/hyperglass/models/config/cache.py index 0871b94..f3c3e93 100644 --- a/hyperglass/models/config/cache.py +++ b/hyperglass/models/config/cache.py @@ -1,6 +1,5 @@ """Validation model for cache config.""" - # Local from ..main import HyperglassModel diff --git a/hyperglass/models/data/bgp_route.py b/hyperglass/models/data/bgp_route.py index dc417ed..a00af4e 100644 --- a/hyperglass/models/data/bgp_route.py +++ b/hyperglass/models/data/bgp_route.py @@ -6,7 +6,7 @@ import typing as t from ipaddress import ip_network # Third Party -from pydantic import field_validator, ValidationInfo +from pydantic import ValidationInfo, field_validator # Project from hyperglass.state import use_state diff --git a/hyperglass/models/parsing/frr.py b/hyperglass/models/parsing/frr.py index 2dbac8a..dacd82c 100644 --- a/hyperglass/models/parsing/frr.py +++ b/hyperglass/models/parsing/frr.py @@ -50,7 +50,7 @@ class FRRPath(_FRRBase): aspath: t.List[int] aggregator_as: int = 0 aggregator_id: str = "" - loc_prf: int = 100 # 100 is the default value for local preference + loc_prf: int = 100 # 100 is the default value for local preference metric: int = 0 med: int = 0 weight: int = 0 @@ -66,7 +66,7 @@ class FRRPath(_FRRBase): """Extract meaningful data from FRR response.""" new = values.copy() new["aspath"] = values["aspath"]["segments"][0]["list"] - community = values.get("community", {'list': []}) + community = values.get("community", {"list": []}) new["community"] = community["list"] new["lastUpdate"] = values["lastUpdate"]["epoch"] bestpath = values.get("bestpath", {}) diff --git a/hyperglass/models/tests/test_util.py b/hyperglass/models/tests/test_util.py index bb133d6..f990d57 100644 --- a/hyperglass/models/tests/test_util.py +++ b/hyperglass/models/tests/test_util.py @@ -19,9 +19,9 @@ def test_check_legacy_fields(): test1_expected.keys() ), "legacy field not replaced" - assert set(check_legacy_fields(model="Device", data=test2).keys()) == set( - test2.keys() - ), "new field not left unmodified" + assert set(check_legacy_fields(model="Device", data=test2).keys()) == set(test2.keys()), ( + "new field not left unmodified" + ) with pytest.raises(ValueError): check_legacy_fields(model="Device", data=test3) diff --git a/hyperglass/plugins/_builtin/__init__.py b/hyperglass/plugins/_builtin/__init__.py index 8f8bd0f..5e87f5b 100644 --- a/hyperglass/plugins/_builtin/__init__.py +++ b/hyperglass/plugins/_builtin/__init__.py @@ -1,12 +1,12 @@ """Built-in hyperglass plugins.""" # Local +from .bgp_route_frr import BGPRoutePluginFrr from .remove_command import RemoveCommand from .bgp_route_arista import BGPRoutePluginArista -from .bgp_route_frr import BGPRoutePluginFrr +from .bgp_route_huawei import BGPRoutePluginHuawei from .bgp_route_juniper import BGPRoutePluginJuniper from .mikrotik_garbage_output import MikrotikGarbageOutput -from .bgp_route_huawei import BGPRoutePluginHuawei __all__ = ( "BGPRoutePluginArista", diff --git a/hyperglass/plugins/_builtin/bgp_route_frr.py b/hyperglass/plugins/_builtin/bgp_route_frr.py index 627fad2..7130b20 100644 --- a/hyperglass/plugins/_builtin/bgp_route_frr.py +++ b/hyperglass/plugins/_builtin/bgp_route_frr.py @@ -62,14 +62,13 @@ def parse_frr(output: t.Sequence[str]) -> "OutputDataModel": return result + class BGPRoutePluginFrr(OutputPlugin): """Coerce a FRR route table in JSON format to a standard BGP Table structure.""" _hyperglass_builtin: bool = PrivateAttr(True) platforms: t.Sequence[str] = ("frr",) - directives: t.Sequence[str] = ( - "__hyperglass_frr_bgp_route_table__", - ) + directives: t.Sequence[str] = ("__hyperglass_frr_bgp_route_table__",) def process(self, *, output: "OutputType", query: "Query") -> "OutputType": """Parse FRR response if data is a string (and is therefore unparsed).""" diff --git a/hyperglass/plugins/_builtin/bgp_route_huawei.py b/hyperglass/plugins/_builtin/bgp_route_huawei.py index 86e70a3..172d906 100644 --- a/hyperglass/plugins/_builtin/bgp_route_huawei.py +++ b/hyperglass/plugins/_builtin/bgp_route_huawei.py @@ -1,36 +1,41 @@ -from ipaddress import ip_network -from .._input import InputPlugin - # Standard Library import typing as t +from ipaddress import ip_network # Third Party from pydantic import PrivateAttr +# Local +from .._input import InputPlugin + if t.TYPE_CHECKING: # Project from hyperglass.models.api.query import Query InputPluginTransformReturn = t.Union[t.Sequence[str], str] + class BGPRoutePluginHuawei(InputPlugin): _hyperglass_builtin: bool = PrivateAttr(True) - platforms: t.Sequence[str] =("huawei", "huawei_vrpv8",) + platforms: t.Sequence[str] = ( + "huawei", + "huawei_vrpv8", + ) directives: t.Sequence[str] = ("__hyperglass_huawei_bgp_route__",) - """ Huawei BGP Route Input Plugin - + This plugin transforms a query target into a network address and prefix length ex.: 192.0.2.0/24 -> 192.0.2.0 24 ex.: 2001:db8::/32 -> 2001:db8:: 32 """ + def transform(self, query: "Query") -> InputPluginTransformReturn: - (target := query.query_target) - + target = query.query_target + if not target or not isinstance(target, list) or len(target) == 0: return None - + target = target[0].strip() # Check for the / in the query target @@ -38,5 +43,5 @@ class BGPRoutePluginHuawei(InputPlugin): return target target_network = ip_network(target) - - return f"{target_network.network_address!s} {target_network.prefixlen!s}" \ No newline at end of file + + return f"{target_network.network_address!s} {target_network.prefixlen!s}" diff --git a/hyperglass/plugins/tests/test_bgp_community.py b/hyperglass/plugins/tests/test_bgp_community.py index d5f8834..3541244 100644 --- a/hyperglass/plugins/tests/test_bgp_community.py +++ b/hyperglass/plugins/tests/test_bgp_community.py @@ -1,4 +1,5 @@ """Test BGP Community validation.""" + # Standard Library import typing as t diff --git a/hyperglass/state/tests/test_hooks.py b/hyperglass/state/tests/test_hooks.py index aaa9d5c..e82f1a4 100644 --- a/hyperglass/state/tests/test_hooks.py +++ b/hyperglass/state/tests/test_hooks.py @@ -96,7 +96,7 @@ def test_use_state_caching(state): instance = use_state(attr) if i == 0: first = instance - assert isinstance( - instance, model - ), f"{instance!r} is not an instance of '{model.__name__}'" + assert isinstance(instance, model), ( + f"{instance!r} is not an instance of '{model.__name__}'" + ) assert instance == first, f"{instance!r} is not equal to {first!r}" diff --git a/hyperglass/ui/pnpm-workspace.yaml b/hyperglass/ui/pnpm-workspace.yaml new file mode 100644 index 0000000..d649e81 --- /dev/null +++ b/hyperglass/ui/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - '@biomejs/biome' + - esbuild From 1e3475a4ad5945ee2693e0f4711bc42283e9ebb3 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sat, 14 Jun 2025 21:44:58 -0400 Subject: [PATCH 15/21] updates ruff config to 'current' standar --- pyproject.toml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c5cf96f..384d6da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,24 @@ exclude = [ "hyperglass/api/examples/*.py", "hyperglass/compat/_sshtunnel.py", ] +line-length = 100 + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint] +fixable=["ALL"] +unfixable=[] +select = ["B", "C", "D", "E", "F", "I", "N", "S", "RET", "W"] ignore = [ # "W503", "RET504", @@ -128,14 +146,6 @@ ignore = [ "B905", # zip without `strict` "W293", # blank line contains whitespace ] -line-length = 100 -select = ["B", "C", "D", "E", "F", "I", "N", "S", "RET", "W"] - -[tool.ruff.pydocstyle] -convention = "pep257" - -[tool.ruff.mccabe] -max-complexity = 10 [tool.ruff.lint.per-file-ignores] "hyperglass/main.py" = ["E402"] From feb5ae8dacd3d98514eba8051873c93d45e0bbea Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sat, 14 Jun 2025 21:56:37 -0400 Subject: [PATCH 16/21] Additional fields for rye --- requirements-dev.lock | 2 ++ requirements.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/requirements-dev.lock b/requirements-dev.lock index 1796da3..e2c1923 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -6,6 +6,8 @@ # features: [] # all-features: false # with-sources: false +# generate-hashes: false +# universal: false -e file:. aiofiles==23.2.1 diff --git a/requirements.lock b/requirements.lock index 016cc74..b151709 100644 --- a/requirements.lock +++ b/requirements.lock @@ -6,6 +6,8 @@ # features: [] # all-features: false # with-sources: false +# generate-hashes: false +# universal: false -e file:. aiofiles==23.2.1 From df684eb8693dfbd2e2aa558a51080c14987d39c5 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sat, 14 Jun 2025 22:01:41 -0400 Subject: [PATCH 17/21] bump pnpm version in docs --- .github/workflows/backend.yml | 2 +- docs/pages/installation/manual.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a45550a..63e4965 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: node-version: [20.x] - pnpm-version: [8] + pnpm-version: [9] redis-version: [latest] python-version: ["3.11", "3.12"] os: [ubuntu-latest] diff --git a/docs/pages/installation/manual.mdx b/docs/pages/installation/manual.mdx index 189d7f4..4c3cefc 100644 --- a/docs/pages/installation/manual.mdx +++ b/docs/pages/installation/manual.mdx @@ -13,7 +13,7 @@ To install hyperglass manually, you'll need to install the following dependencie 1. [Python 3.11, or 3.12](https://www.python.org/downloads/) and [`pip`](https://pip.pypa.io/en/stable/installation/) 2. [NodeJS 20.14 or later](https://nodejs.org/en/download) -3. [PNPM 8 or later](https://pnpm.io/installation) +3. [PNPM 9 or later](https://pnpm.io/installation) 4. [Redis 7.2 or later](https://redis.io/download/) Make sure the Redis server is started. From c68b00423a5de9fbff01cab034f9d6a93f983d1c Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sat, 14 Jun 2025 22:17:03 -0400 Subject: [PATCH 18/21] Update pnpm-workspace.yaml --- hyperglass/ui/pnpm-workspace.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hyperglass/ui/pnpm-workspace.yaml b/hyperglass/ui/pnpm-workspace.yaml index d649e81..c70f143 100644 --- a/hyperglass/ui/pnpm-workspace.yaml +++ b/hyperglass/ui/pnpm-workspace.yaml @@ -1,3 +1,5 @@ onlyBuiltDependencies: - '@biomejs/biome' - esbuild +packages: + - . \ No newline at end of file From 8f690adb5c68be19ed13dfdefee5906033a8007d Mon Sep 17 00:00:00 2001 From: Chumy Date: Mon, 28 Oct 2024 07:55:57 +0800 Subject: [PATCH 19/21] Fix bug: Directives will be error on deserialize rules and field with None type --- hyperglass/models/directive.py | 30 ++++++++++--------- hyperglass/models/ui.py | 2 +- .../ui/components/looking-glass-form.tsx | 11 +++++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/hyperglass/models/directive.py b/hyperglass/models/directive.py index 058eaae..c88c2c1 100644 --- a/hyperglass/models/directive.py +++ b/hyperglass/models/directive.py @@ -19,7 +19,7 @@ from .main import MultiModel, HyperglassModel, HyperglassUniqueModel from .fields import Action StringOrArray = t.Union[str, t.List[str]] -Condition = t.Union[IPvAnyNetwork, str] +Condition = t.Union[str, None] RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None] PassedValidation = t.Union[bool, None] IPFamily = t.Literal["ipv4", "ipv6"] @@ -264,7 +264,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): id: str name: str rules: t.List[RuleType] = [RuleWithoutValidation()] - field: t.Union[Text, Select] + field: t.Union[Text, Select, None] info: t.Optional[FilePath] = None plugins: t.List[str] = [] table_output: t.Optional[str] = None @@ -282,15 +282,16 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): condition = rule.get("condition") if condition is None: out_rules.append(RuleWithoutValidation(**rule)) - try: - condition_net = ip_network(condition) - if condition_net.version == 4: - out_rules.append(RuleWithIPv4(**rule)) - if condition_net.version == 6: - out_rules.append(RuleWithIPv6(**rule)) - except ValueError: - out_rules.append(RuleWithPattern(**rule)) - if isinstance(rule, Rule): + else: + try: + condition_net = ip_network(condition) + if condition_net.version == 4: + out_rules.append(RuleWithIPv4(**rule)) + if condition_net.version == 6: + out_rules.append(RuleWithIPv6(**rule)) + except ValueError: + out_rules.append(RuleWithPattern(**rule)) + elif isinstance(rule, Rule): out_rules.append(rule) return out_rules @@ -306,7 +307,8 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): @property def field_type(self) -> t.Literal["text", "select", None]: """Get the linked field type.""" - + if self.field is None: + return None if self.field.is_select: return "select" if self.field.is_text or self.field.is_ip: @@ -337,7 +339,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): "name": self.name, "field_type": self.field_type, "groups": self.groups, - "description": self.field.description, + "description": self.field.description if self.field is not None else '', "info": None, } @@ -345,7 +347,7 @@ class Directive(HyperglassUniqueModel, unique_by=("id", "table_output")): with self.info.open() as md: value["info"] = md.read() - if self.field.is_select: + if self.field is not None and self.field.is_select: value["options"] = [o.export_dict() for o in self.field.options if o is not None] return value diff --git a/hyperglass/models/ui.py b/hyperglass/models/ui.py index 3e2c106..f8a304c 100644 --- a/hyperglass/models/ui.py +++ b/hyperglass/models/ui.py @@ -19,7 +19,7 @@ class UIDirective(HyperglassModel): id: str name: str - field_type: str + field_type: t.Union[str, None] groups: t.List[str] description: str info: t.Optional[str] = None diff --git a/hyperglass/ui/components/looking-glass-form.tsx b/hyperglass/ui/components/looking-glass-form.tsx index 664f8a8..334795a 100644 --- a/hyperglass/ui/components/looking-glass-form.tsx +++ b/hyperglass/ui/components/looking-glass-form.tsx @@ -79,7 +79,14 @@ export const LookingGlassForm = (): JSX.Element => { ); const directive = useMemo( - () => getDirective(), + () => { + const tmp = getDirective(); + if (tmp !== null && tmp.fieldType === null) { + setFormValue('queryTarget', ['null']); + setValue('queryTarget', ['null']); + } + return tmp; + }, [form.queryType, form.queryLocation, getDirective], ); @@ -200,7 +207,7 @@ export const LookingGlassForm = (): JSX.Element => { - + {directive !== null && ( Date: Thu, 25 Sep 2025 23:19:34 -0400 Subject: [PATCH 20/21] updates CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9baaa6c..692344c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Fixed - +- [#280](https://github.com/thatmattlove/hyperglass/issues/280): Fix: `condition: None` caused error in directive - [#306](https://github.com/thatmattlove/hyperglass/issues/306): Fix: allow integer values in ext_community_list_raw field for Arista BGP - @cooperwinser - [#325](https://github.com/thatmattlove/hyperglass/pull/325): Fix code block padding in the documentation - @jagardaniel - [#327](https://github.com/thatmattlove/hyperglass/pull/327): Fix huawei bgp route and plugin validation/transform order - @JelsonRodrigues From b52a38369182a72adf568962ca36eab4674630bd Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 25 Sep 2025 23:29:18 -0400 Subject: [PATCH 21/21] fixes formatting --- .../ui/components/looking-glass-form.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/hyperglass/ui/components/looking-glass-form.tsx b/hyperglass/ui/components/looking-glass-form.tsx index 334795a..2da4633 100644 --- a/hyperglass/ui/components/looking-glass-form.tsx +++ b/hyperglass/ui/components/looking-glass-form.tsx @@ -78,17 +78,14 @@ export const LookingGlassForm = (): JSX.Element => { [], ); - const directive = useMemo( - () => { - const tmp = getDirective(); - if (tmp !== null && tmp.fieldType === null) { - setFormValue('queryTarget', ['null']); - setValue('queryTarget', ['null']); - } - return tmp; - }, - [form.queryType, form.queryLocation, getDirective], - ); + const directive = useMemo(() => { + const tmp = getDirective(); + if (tmp !== null && tmp.fieldType === null) { + setFormValue('queryTarget', ['null']); + setValue('queryTarget', ['null']); + } + return tmp; + }, [form.queryType, form.queryLocation, getDirective]); function submitHandler(): void { if (process.env.NODE_ENV === 'development') { @@ -207,7 +204,11 @@ export const LookingGlassForm = (): JSX.Element => { - + {directive !== null && (