diff --git a/group_vars/prom/prometheus/quanta.yml b/group_vars/prom/prometheus/quanta.yml index cedd93b..682d062 100644 --- a/group_vars/prom/prometheus/quanta.yml +++ b/group_vars/prom/prometheus/quanta.yml @@ -68,7 +68,7 @@ prometheus__rules_quanta: severity: critical - alert: QuantaIntakeTemp expr: - 0.5 * snAgentTempValue{snAgentTempSensorDescr=~".*Intake.*"} > 45 + 0.5 * snAgentTempValue{snAgentTempSensorDescr=~".*Intake.*"} > 60 for: 10m keep_firing_for: 30m labels: @@ -78,7 +78,7 @@ prometheus__rules_quanta: Description: !unsafe "{{ $labels.snAgentTempSensorDescr }}" - alert: QuantaIntakeTemp expr: - 0.5 * snAgentTempValue{snAgentTempSensorDescr=~".*Intake.*"} > 60 + 0.5 * snAgentTempValue{snAgentTempSensorDescr=~".*Intake.*"} > 70 for: 10m keep_firing_for: 30m labels: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a8f43fe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 79 diff --git a/roles/bird/filter_plugins/bird.py b/roles/bird/filter_plugins/bird.py index 504178c..00f38bb 100644 --- a/roles/bird/filter_plugins/bird.py +++ b/roles/bird/filter_plugins/bird.py @@ -1,8 +1,9 @@ from __future__ import annotations import itertools +import re from dataclasses import dataclass -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv4Network, IPv6Network, ip_network from typing import Any, Generic, Iterator, Literal, TypeVar from pydantic import ( @@ -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"(? 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,53 @@ 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 IPFlag: + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + pattern = r"(?P.*?)(?P[+-]|\{[0-9]+,[0-9]+\})?$" + parts = re.match(pattern, v) + + return (ip_network(parts.group("ip")), parts.group("flag") or "") + + +class NetMatch(BaseModel): + matches: list[IPFlag] = Field(alias="net.match") + + +class NetLength(BaseModel): + length: IPv4orIPv6 = Field(alias="net.len") + + +Condition = ( + Proto + | Source + | And + | Or + | Not + | AsPathContains + | AsPathLength + | NetLength + | NetMatch +) And.update_forward_refs() Or.update_forward_refs() @@ -61,6 +132,10 @@ Accept = Literal["accept"] Reject = Literal["reject"] +class RejectWithMsg(BaseModel): + reject: str + + class PrefSrc(BaseModel): pref_src: AutoList[IPvAnyAddress] @@ -70,7 +145,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 +219,37 @@ 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}" + + case NetMatch(matches=matches): + networkType = IPv4Network if ctx.ipv4 else IPv6Network + networks = [m for m in matches if isinstance(m[0], networkType)] + + return f"net ~ [ {', '.join([f'{network}{flag}' for (network, flag) in networks])} ]" + 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( diff --git a/roles/bird/templates/bird.conf.j2 b/roles/bird/templates/bird.conf.j2 index e5884d4..59dbe32 100644 --- a/roles/bird/templates/bird.conf.j2 +++ b/roles/bird/templates/bird.conf.j2 @@ -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 %}