feat(bird): Add as_path.{contains,len}, net.len

This commit is contained in:
v-lafeychine 2024-03-31 18:12:38 +02:00
parent 7f9ccf3e59
commit 8d0139925e
Signed by: v-lafeychine
GPG key ID: F46CAAD27C7AB0D5
2 changed files with 89 additions and 20 deletions

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import re
from dataclasses import dataclass from dataclasses import dataclass
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Any, Generic, Iterator, Literal, TypeVar from typing import Any, Generic, Iterator, Literal, TypeVar
@ -29,6 +30,30 @@ class AutoList(list[T], Generic[T]):
return [parse_obj_as(T, value)] return [parse_obj_as(T, value)]
VARIABLES = {
"net.len": "net.len",
}
def interpolate(string: str, ctx: Context) -> str:
pattern = r"(?<!\\)(\$\{[_a-z][_a-z0-9.]*\})"
def lookup(var: str) -> str:
try:
return VARIABLES[var]
except KeyError:
return quoted(getattr(ctx, var))
split = re.split(pattern, string)
parts = [
(lookup(p[2:-1]) if re.match(pattern, p) else quoted(p))
for p in split
if p
]
return ", ".join(parts)
class Proto(BaseModel): class Proto(BaseModel):
protos: AutoList[str] protos: AutoList[str]
@ -49,7 +74,28 @@ class Not(BaseModel):
condition: Condition = Field(alias="not") condition: Condition = Field(alias="not")
Condition = Proto | Source | And | Or | Not class AsPathContains(BaseModel):
contains: AutoList[int] = Field(alias="as_path.contains")
class AsPathLength(BaseModel):
length: list[int] = Field(
ge=0, min_items=2, max_items=2, alias="as_path.len"
)
class IPv4orIPv6(BaseModel):
ipv4: list[int] = Field(ge=0, min_items=2, max_items=2)
ipv6: list[int] = Field(ge=0, min_items=2, max_items=2)
class NetLength(BaseModel):
length: IPv4orIPv6 = Field(alias="net.len")
Condition = (
Proto | Source | And | Or | Not | AsPathContains | AsPathLength | NetLength
)
And.update_forward_refs() And.update_forward_refs()
Or.update_forward_refs() Or.update_forward_refs()
@ -61,6 +107,10 @@ Accept = Literal["accept"]
Reject = Literal["reject"] Reject = Literal["reject"]
class RejectWithMsg(BaseModel):
reject: str
class PrefSrc(BaseModel): class PrefSrc(BaseModel):
pref_src: AutoList[IPvAnyAddress] pref_src: AutoList[IPvAnyAddress]
@ -70,7 +120,7 @@ class Conditional(BaseModel):
actions: AutoList[Action] = Field(alias="then") actions: AutoList[Action] = Field(alias="then")
Action = Accept | Reject | PrefSrc | Conditional Action = Accept | Reject | RejectWithMsg | PrefSrc | Conditional
Conditional.update_forward_refs() Conditional.update_forward_refs()
@ -144,12 +194,31 @@ def str_of_condition(condition: Condition, ctx: bool) -> str:
sources = [str(s) for s in sources] sources = [str(s) for s in sources]
return f"krt_source ~ [ {', '.join(sources)} ]" return f"krt_source ~ [ {', '.join(sources)} ]"
case AsPathContains(contains=contains):
return (
f"bgp_path ~ [ {', '.join([str(asn) for asn in contains])} ]"
)
case AsPathLength(length=[min_len, max_len]):
return f"{min_len} <= bgp_path.len && bgp_path.len <= {max_len}"
case NetLength(
length=IPv4orIPv6(ipv4=[min_v4, max_v4], ipv6=[min_v6, max_v6])
):
if ctx.ipv4:
return f"{min_v4} <= net.len && net.len <= {max_v4}"
else:
return f"{min_v6} <= net.len && net.len <= {max_v6}"
def lines_of_action(action: Action, ctx: Context) -> Iterable[str]: def lines_of_action(action: Action, ctx: Context) -> Iterable[str]:
match action: match action:
case "accept" | "reject": case "accept" | "reject":
yield f"{action};" yield f"{action};"
case RejectWithMsg(reject=reason):
yield f"reject {interpolate(reason, ctx)};"
case Conditional(condition=condition, actions=actions): case Conditional(condition=condition, actions=actions):
yield f"if {str_of_condition(condition, ctx)} then {'{'}" yield f"if {str_of_condition(condition, ctx)} then {'{'}"
yield from indent( yield from indent(

View file

@ -27,8 +27,8 @@ protocol device {
{% for name, kernel in bird__kernel.items() %} {% for name, kernel in bird__kernel.items() %}
{% for version in ["ipv4", "ipv6"] %} {% for version in ["ipv4", "ipv6"] %}
{% set ipv4 = version == "ipv4" %} {% set is_ipv4 = version == "ipv4" %}
protocol kernel {{ name | bird_name(ipv4) }} { protocol kernel {{ name | bird_name(is_ipv4) }} {
{% if kernel.kernel is defined %} {% if kernel.kernel is defined %}
kernel table {{ kernel.kernel }}; kernel table {{ kernel.kernel }};
{% endif %} {% endif %}
@ -40,9 +40,9 @@ protocol kernel {{ name | bird_name(ipv4) }} {
{% endif %} {% endif %}
{{ version }} { {{ version }} {
{% if kernel.table is defined %} {% if kernel.table is defined %}
table {{ kernel.table | bird_name(ipv4) }}; table {{ kernel.table | bird_name(is_ipv4) }};
{% endif %} {% endif %}
{{ import_export(kernel, ipv4) | indent(8) }} {{ import_export(kernel, is_ipv4) | indent(8) }}
}; };
} }
{% endfor %} {% endfor %}
@ -50,25 +50,25 @@ protocol kernel {{ name | bird_name(ipv4) }} {
{% for name, pipe in bird__pipes.items() %} {% for name, pipe in bird__pipes.items() %}
{% for version in ["ipv4", "ipv6"] %} {% for version in ["ipv4", "ipv6"] %}
{% set ipv4 = version == "ipv4" %} {% set is_ipv4 = version == "ipv4" %}
protocol pipe {{ name | bird_name(ipv4) }} { protocol pipe {{ name | bird_name(is_ipv4) }} {
table {{ pipe.table | bird_name(ipv4) }}; table {{ pipe.table | bird_name(is_ipv4) }};
peer table {{ pipe.peer_table | default("master") | bird_name(ipv4) }}; peer table {{ pipe.peer_table | default("master") | bird_name(is_ipv4) }};
{{ import_export(kernel, ipv4) | indent(4) }} {{ import_export(kernel, is_ipv4) | indent(4) }}
} }
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% if bird__ospf is defined %} {% if bird__ospf is defined %}
{% for version in ["ipv4", "ipv6"] %} {% for version in ["ipv4", "ipv6"] %}
{% set ipv4 = version == "ipv4" %} {% set is_ipv4 = version == "ipv4" %}
{% set ospf_version = "v2" if ipv4 else "v3" %} {% set ospf_version = "v2" if is_ipv4 else "v3" %}
protocol ospf {{ ospf_version }} {{ "ospf" | bird_name(ipv4) }} { protocol ospf {{ ospf_version }} {{ "ospf" | bird_name(is_ipv4) }} {
{{ version }} { {{ version }} {
{% if bird__ospf.table is defined %} {% if bird__ospf.table is defined %}
table {{ bird__ospf.table | bird_name(ipv4) }}; table {{ bird__ospf.table | bird_name(is_ipv4) }};
{% endif %} {% endif %}
{{ import_export(bird__ospf, ipv4) | indent(8) }} {{ import_export(bird__ospf, is_ipv4) | indent(8) }}
}; };
{% for id, area in bird__ospf.areas.items() %} {% for id, area in bird__ospf.areas.items() %}
area {{ id }} { area {{ id }} {
@ -92,8 +92,8 @@ protocol ospf {{ ospf_version }} {{ "ospf" | bird_name(ipv4) }} {
{% for name, bgp in bird__bgp.items() %} {% for name, bgp in bird__bgp.items() %}
{% for version in ["ipv4", "ipv6"] %} {% for version in ["ipv4", "ipv6"] %}
{% set ipv4 = version == "ipv4" %} {% set is_ipv4 = version == "ipv4" %}
protocol bgp {{ name | bird_name(ipv4) }} { protocol bgp {{ name | bird_name(is_ipv4) }} {
local {{ bgp.local.address local {{ bgp.local.address
| ansible.utils.ipaddr(version) | ansible.utils.ipaddr(version)
| first }} as {{ bgp.local.as }}; | first }} as {{ bgp.local.as }};
@ -106,12 +106,12 @@ protocol bgp {{ name | bird_name(ipv4) }} {
{% endif %} {% endif %}
{{ version }} { {{ version }} {
{% if bgp.table is defined %} {% if bgp.table is defined %}
table {{ bgp.table | bird_name(ipv4) }}; table {{ bgp.table | bird_name(is_ipv4) }};
{% endif %} {% endif %}
{% if bgp.next_hop_self is defined %} {% if bgp.next_hop_self is defined %}
next hop self; next hop self;
{% endif %} {% endif %}
{{ import_export(bgp, ipv4) | indent(8) }} {{ import_export(bgp, is_ipv4) | indent(8) }}
}; };
} }
{% endfor %} {% endfor %}