1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-05-07 12:43:05 +00:00

feat: Add BGP community friendly names and enhance RPKI configuration

- Add new 'name' mode for BGP communities to append friendly names
  - New configuration option `structured.communities.mode: name`
  - Community mappings via `structured.communities.names` dictionary
  - Communities display as "65000:1000 - Upstream Any" in UI
  - Backward compatible with existing permit/deny modes

- Enhance RPKI configuration documentation
  - Document both Cloudflare and Routinator backend options
  - Add `structured.rpki.backend` and `structured.rpki.rpki_server_url` parameters
  - Clarify Routinator web API endpoint usage vs RTR port
  - Add comprehensive configuration examples

- Update structured output platform support
  - Document all supported platforms: Arista EOS, FRRouting, Huawei VRP, Juniper Junos, Mikrotik RouterOS/SwitchOS

- Frontend enhancements
  - Parse comma-separated community format in UI components
  - Display friendly names alongside community codes
  - Maintain existing functionality for communities without names

- Add validation and examples
  - Validate that 'name' mode has community mappings configured
  - Include example configuration and test cases
  - Generic examples using ASN 65000 instead of specific networks
This commit is contained in:
Wilhelm Schonfeldt 2025-09-26 11:32:29 +02:00
parent e78685d8c6
commit eabd98b606
6 changed files with 250 additions and 14 deletions

View file

@ -1,22 +1,34 @@
import { Callout } from "nextra/components";
## Structured
Devices that support responding to a query with structured or easily parsable data can have their response data placed into an easier to read table (or JSON, when using the REST API). Currently, the following platforms have structured data supported in hyperglass:
- Arista EOS
- FRRouting
- Huawei VRP
- Juniper Junos
- Mikrotik RouterOS/SwitchOS
When structured output is available, hyperglass checks the RPKI state of each BGP prefix returned using one of two methods:
1. From the router's perspective
2. From the perspective of [Cloudflare's RPKI Service](https://rpki.cloudflare.com/)
1. **From the router's perspective** - Uses the RPKI validation state as determined by the router itself
2. **From an external RPKI validation service** - Queries an external service to determine RPKI state independently
For external validation, hyperglass supports two backends:
- **Cloudflare**: Uses [Cloudflare's public RPKI service](https://rpki.cloudflare.com/) via GraphQL API
- **Routinator**: Connects to your own [Routinator](https://github.com/NLnetLabs/routinator) RPKI validator instance
Additionally, hyperglass provides the ability to control which BGP communities are shown to the end user.
| Parameter | Type | Default Value | Description |
| :----------------------------- | :-------------- | :------------ | :---------------------------------------------------------------------------------------------------------------------------- |
| `structured.rpki.mode` | String | router | Use `router` to use the router's view of the RPKI state (1 above), or `external` to use Cloudflare's view (2 above). |
| `structured.communities.mode` | String | deny | Use `deny` to deny any communities listed in `structured.communities.items`, or `permit` to _only_ permit communities listed. |
| `structured.communities.items` | List of Strings | | List of communities to match. |
| Parameter | Type | Default Value | Description |
| :----------------------------- | :-------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------- |
| `structured.rpki.mode` | String | router | Use `router` to use the router's view of the RPKI state, or `external` to use an external validation service. |
| `structured.rpki.backend` | String | cloudflare | When using `external` mode, choose `cloudflare` or `routinator` as the validation backend. |
| `structured.rpki.rpki_server_url` | String | | When using `routinator` backend, specify the base URL of your Routinator server (e.g., `http://rpki.example.com:3323`). |
| `structured.communities.mode` | String | deny | Use `deny` to deny any communities listed, `permit` to _only_ permit communities listed, or `name` to append friendly names. |
| `structured.communities.items` | List of Strings | | List of communities to match (used by `deny` and `permit` modes). |
| `structured.communities.names` | Dict | | Dictionary mapping BGP community codes to friendly names (used by `name` mode). |
### RPKI Examples
@ -28,14 +40,31 @@ structured:
mode: router
```
#### Show RPKI State from a Public/External Perspective
#### Show RPKI State from Cloudflare's Public Service
```yaml filename="config.yaml" copy {2}
```yaml filename="config.yaml" copy {2-4}
structured:
rpki:
mode: external
backend: cloudflare
```
#### Show RPKI State from a Custom Routinator Server
```yaml filename="config.yaml" copy {2-5}
structured:
rpki:
mode: external
backend: routinator
rpki_server_url: "http://rpki.example.com:8080"
```
<Callout type="info" emoji="">
**Routinator URL Format**
The `rpki_server_url` should be the base URL of your Routinator HTTP web API endpoint. This is typically different from the RTR port (3323). The URL should not include the `/validity` path as hyperglass will append this automatically.
</Callout>
### Community Filtering Examples
#### Deny Listed Communities by Regex pattern
@ -59,3 +88,19 @@ structured:
- "^65000:.*$" # permit any communities starting with 65000, but no others.
- "1234:1" # permit only the 1234:1 community.
```
#### Append Friendly Names to Communities
```yaml filename="config.yaml" {2-10}
structured:
communities:
mode: name
names:
"65000:1000": "Upstream Any"
"65000:1001": "Upstream A (all locations)"
"65000:1101": "Upstream A Location 1"
"65000:1201": "Upstream A Location 2"
"65000:1002": "Upstream B (all locations)"
"65000:1102": "Upstream B Location 1"
"65000:2000": "IXP Any"
```

View file

@ -0,0 +1,21 @@
# Example configuration using the new 'name' mode for BGP communities
# This would typically go in your main config.yaml file
structured:
communities:
mode: name
names:
"65000:1000:0": "Upstream Any"
"65000:1001:0": "Upstream A (all locations)"
"65000:1101:0": "Upstream A Location 1"
"65000:1201:0": "Upstream A Location 2"
"65000:1002:0": "Upstream B (all locations)"
"65000:1102:0": "Upstream B Location 1"
"65000:2000:0": "IXP Any"
# With this configuration:
# - BGP communities that appear in the output and match the keys in 'names'
# will be displayed with friendly names appended
# - For example: "65000:1000:0" becomes "65000:1000:0,Upstream Any"
# - Communities without mappings remain unchanged
# - The frontend will display them as "65000:1000:0 - Upstream Any"

View file

@ -3,10 +3,13 @@
# Standard Library
import typing as t
# Third Party
from pydantic import field_validator, ValidationInfo
# Local
from ..main import HyperglassModel
StructuredCommunityMode = t.Literal["permit", "deny"]
StructuredCommunityMode = t.Literal["permit", "deny", "name"]
StructuredRPKIMode = t.Literal["router", "external"]
@ -15,6 +18,14 @@ class StructuredCommunities(HyperglassModel):
mode: StructuredCommunityMode = "deny"
items: t.List[str] = []
names: t.Dict[str, str] = {}
@field_validator("names")
def validate_names(cls, value: t.Dict[str, str], info: ValidationInfo) -> t.Dict[str, str]:
"""Validate that names are provided when mode is 'name'."""
if info.data and info.data.get("mode") == "name" and not value:
raise ValueError("When using mode 'name', at least one community mapping must be provided in 'names'")
return value
class StructuredRpki(HyperglassModel):

View file

@ -42,6 +42,7 @@ class BGPRoute(HyperglassModel):
Actions:
permit: only permit matches
deny: only deny matches
name: append friendly names to matching communities
"""
(structured := use_state("params").structured)
@ -64,10 +65,21 @@ class BGPRoute(HyperglassModel):
break
return valid
func_map = {"permit": _permit, "deny": _deny}
func = func_map[structured.communities.mode]
def _name(comm):
"""Append friendly names to matching communities."""
# Check if this community has a friendly name mapping
if comm in structured.communities.names:
return f"{comm},{structured.communities.names[comm]}"
return comm
return [c for c in value if func(c)]
if structured.communities.mode == "name":
# For name mode, transform communities to include friendly names
return [_name(c) for c in value]
else:
# For permit/deny modes, use existing filtering logic
func_map = {"permit": _permit, "deny": _deny}
func = func_map[structured.communities.mode]
return [c for c in value if func(c)]
@field_validator("rpki_state")
def validate_rpki_state(cls, value, info: ValidationInfo):

View file

@ -149,6 +149,16 @@ export const Communities = (props: CommunitiesProps): JSX.Element => {
const { web } = useConfig();
const bg = useColorValue('white', 'gray.900');
const color = useOpposingColor(bg);
// Parse communities to separate code and name if present
const parsedCommunities = communities.map(community => {
if (community.includes(',')) {
const [code, name] = community.split(',', 2);
return { code, name, display: `${code} - ${name}` };
}
return { code: community, name: null, display: community };
});
return (
<If condition={communities.length === 0}>
<Then>
@ -175,7 +185,9 @@ export const Communities = (props: CommunitiesProps): JSX.Element => {
fontWeight="normal"
whiteSpace="pre-wrap"
>
{communities.join('\n')}
{parsedCommunities.map(({ display }, index) => (
<Text key={index} as="div">{display}</Text>
))}
</MenuList>
</Menu>
</Else>

135
test_community_names.py Normal file
View file

@ -0,0 +1,135 @@
"""Test the new name mode for community validation."""
# Standard Library
import typing as t
from unittest.mock import Mock
# Third Party
from pydantic import ValidationError
# Project
from hyperglass.models.data.bgp_route import BGPRoute
from hyperglass.models.config.structured import StructuredCommunities
def test_community_validation_name_mode():
"""Test that name mode correctly appends friendly names to communities."""
# Mock the state to return our test configuration
from hyperglass import state
# Create a mock structured config with name mode
mock_structured = Mock()
mock_structured.communities = StructuredCommunities(
mode="name",
names={
"65000:1000:0": "Upstream Any",
"65000:1001:0": "Upstream A (all locations)",
"65000:1": "Test Community"
}
)
# Mock the params with our structured config
mock_params = Mock()
mock_params.structured = mock_structured
# Mock the use_state function to return our mock params
original_use_state = getattr(state, 'use_state', None)
state.use_state = Mock(return_value=mock_params)
try:
# Test data for BGP route
test_data = {
"prefix": "192.0.2.0/24",
"active": True,
"age": 3600,
"weight": 100,
"med": 0,
"local_preference": 100,
"as_path": [65000, 65001],
"communities": [
"65000:1000:0", # Should get friendly name
"65000:1001:0", # Should get friendly name
"65000:9999:0", # Should remain unchanged (no mapping)
"65000:1", # Should get friendly name
],
"next_hop": "192.0.2.1",
"source_as": 65001,
"source_rid": "192.0.2.1",
"peer_rid": "192.0.2.2",
"rpki_state": 1
}
# Create BGPRoute instance
route = BGPRoute(**test_data)
# Check that communities have been transformed correctly
expected_communities = [
"65000:1000:0,Upstream Any",
"65000:1001:0,Upstream A (all locations)",
"65000:9999:0", # No friendly name, stays unchanged
"65000:1,Test Community"
]
assert route.communities == expected_communities
finally:
# Restore original use_state function
if original_use_state:
state.use_state = original_use_state
def test_community_validation_permit_mode_unchanged():
"""Test that permit mode still works as before."""
from hyperglass import state
# Create a mock structured config with permit mode
mock_structured = Mock()
mock_structured.communities = StructuredCommunities(
mode="permit",
items=["^65000:.*$", "1234:1"]
)
mock_params = Mock()
mock_params.structured = mock_structured
original_use_state = getattr(state, 'use_state', None)
state.use_state = Mock(return_value=mock_params)
try:
test_data = {
"prefix": "192.0.2.0/24",
"active": True,
"age": 3600,
"weight": 100,
"med": 0,
"local_preference": 100,
"as_path": [65000, 65001],
"communities": [
"65000:100", # Should be permitted (matches ^65000:.*$)
"65001:200", # Should be denied (doesn't match patterns)
"1234:1", # Should be permitted (exact match)
],
"next_hop": "192.0.2.1",
"source_as": 65001,
"source_rid": "192.0.2.1",
"peer_rid": "192.0.2.2",
"rpki_state": 1
}
route = BGPRoute(**test_data)
# Should only include permitted communities
expected_communities = ["65000:100", "1234:1"]
assert route.communities == expected_communities
finally:
if original_use_state:
state.use_state = original_use_state
if __name__ == "__main__":
test_community_validation_name_mode()
test_community_validation_permit_mode_unchanged()
print("All tests passed!")