Compare commits

...

2 commits

3 changed files with 91 additions and 20 deletions

2
pyproject.toml Normal file
View file

@ -0,0 +1,2 @@
[tool.black]
line-length = 79

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import itertools
import re
from dataclasses import dataclass
from ipaddress import IPv4Address
from typing import Any, Generic, Iterator, Literal, TypeVar
@ -29,6 +30,30 @@ class AutoList(list[T], Generic[T]):
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):
protos: AutoList[str]
@ -49,7 +74,28 @@ class Not(BaseModel):
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()
Or.update_forward_refs()
@ -61,6 +107,10 @@ Accept = Literal["accept"]
Reject = Literal["reject"]
class RejectWithMsg(BaseModel):
reject: str
class PrefSrc(BaseModel):
pref_src: AutoList[IPvAnyAddress]
@ -70,7 +120,7 @@ class Conditional(BaseModel):
actions: AutoList[Action] = Field(alias="then")
Action = Accept | Reject | PrefSrc | Conditional
Action = Accept | Reject | RejectWithMsg | PrefSrc | Conditional
Conditional.update_forward_refs()
@ -144,12 +194,31 @@ def str_of_condition(condition: Condition, ctx: bool) -> str:
sources = [str(s) for s in 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]:
match action:
case "accept" | "reject":
yield f"{action};"
case RejectWithMsg(reject=reason):
yield f"reject {interpolate(reason, ctx)};"
case Conditional(condition=condition, actions=actions):
yield f"if {str_of_condition(condition, ctx)} then {'{'}"
yield from indent(

View file

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