annet 0.16.16__py3-none-any.whl → 0.16.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of annet might be problematic. Click here for more details.
- annet/configs/context.yml +1 -0
- annet/rpl/__init__.py +27 -0
- annet/rpl/action.py +51 -0
- annet/rpl/condition.py +94 -0
- annet/rpl/match_builder.py +103 -0
- annet/rpl/policy.py +22 -0
- annet/rpl/result.py +8 -0
- annet/rpl/routemap.py +76 -0
- annet/rpl/statement_builder.py +267 -0
- annet/rulebook/cisco/misc.py +92 -0
- annet/rulebook/texts/arista.rul +1 -0
- annet/rulebook/texts/cisco.rul +1 -1
- annet-0.16.18.dist-info/METADATA +26 -0
- {annet-0.16.16.dist-info → annet-0.16.18.dist-info}/RECORD +23 -11
- {annet-0.16.16.dist-info → annet-0.16.18.dist-info}/WHEEL +1 -1
- annet_generators/rpl_example/__init__.py +9 -0
- annet_generators/rpl_example/items.py +31 -0
- annet_generators/rpl_example/policy_generator.py +233 -0
- annet_generators/rpl_example/route_policy.py +33 -0
- annet-0.16.16.dist-info/METADATA +0 -27
- {annet-0.16.16.dist-info → annet-0.16.18.dist-info}/AUTHORS +0 -0
- {annet-0.16.16.dist-info → annet-0.16.18.dist-info}/LICENSE +0 -0
- {annet-0.16.16.dist-info → annet-0.16.18.dist-info}/entry_points.txt +0 -0
- {annet-0.16.16.dist-info → annet-0.16.18.dist-info}/top_level.txt +0 -0
annet/configs/context.yml
CHANGED
annet/rpl/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"MatchField",
|
|
3
|
+
"ThenField",
|
|
4
|
+
"RouteMap",
|
|
5
|
+
"Route",
|
|
6
|
+
"ResultType",
|
|
7
|
+
"ActionType",
|
|
8
|
+
"Action",
|
|
9
|
+
"SingleAction",
|
|
10
|
+
"AndCondition",
|
|
11
|
+
"R",
|
|
12
|
+
"ConditionOperator",
|
|
13
|
+
"Condition",
|
|
14
|
+
"SingleCondition",
|
|
15
|
+
"RoutingPolicyStatement",
|
|
16
|
+
"RoutingPolicy",
|
|
17
|
+
"CommunityActionValue",
|
|
18
|
+
"PrefixMatchValue",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
from .action import Action, ActionType, SingleAction
|
|
22
|
+
from .condition import AndCondition, Condition, ConditionOperator, SingleCondition
|
|
23
|
+
from .match_builder import R, MatchField, PrefixMatchValue
|
|
24
|
+
from .policy import RoutingPolicyStatement, RoutingPolicy
|
|
25
|
+
from .result import ResultType
|
|
26
|
+
from .routemap import RouteMap, Route
|
|
27
|
+
from .statement_builder import ThenField, CommunityActionValue
|
annet/rpl/action.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from collections.abc import Iterator, Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Generic, TypeVar, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ActionType(Enum):
|
|
8
|
+
SET = "set"
|
|
9
|
+
ADD = "add"
|
|
10
|
+
REMOVE = "delete"
|
|
11
|
+
|
|
12
|
+
CUSTOM = "custom"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ValueT = TypeVar("ValueT")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SingleAction(Generic[ValueT]):
|
|
20
|
+
field: str
|
|
21
|
+
type: ActionType
|
|
22
|
+
value: ValueT
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Action:
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.actions: list[SingleAction[Any]] = []
|
|
28
|
+
|
|
29
|
+
def append(self, action: SingleAction[Any]) -> None:
|
|
30
|
+
self.actions.append(action)
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
actions = ", ".join(repr(c) for c in self.actions)
|
|
34
|
+
return f"Action({actions})"
|
|
35
|
+
|
|
36
|
+
def __getitem__(self, item: str) -> SingleAction[Any]:
|
|
37
|
+
return next(c for c in self.actions if c.field == item)
|
|
38
|
+
|
|
39
|
+
def __contains__(self, item: str) -> bool:
|
|
40
|
+
return any(c.field == item for c in self.actions)
|
|
41
|
+
|
|
42
|
+
def __len__(self) -> int:
|
|
43
|
+
return len(self.actions)
|
|
44
|
+
|
|
45
|
+
def __iter__(self) -> Iterator[SingleAction[Any]]:
|
|
46
|
+
return iter(self.actions)
|
|
47
|
+
|
|
48
|
+
def find_all(self, item: str) -> Iterable[SingleAction[Any]]:
|
|
49
|
+
for action in self.actions:
|
|
50
|
+
if action.field == item:
|
|
51
|
+
yield action
|
annet/rpl/condition.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from collections.abc import Iterator, Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Generic, TypeVar, Sequence, Union, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConditionOperator(Enum):
|
|
8
|
+
EQ = "=="
|
|
9
|
+
GE = ">="
|
|
10
|
+
GT = ">"
|
|
11
|
+
LE = "<="
|
|
12
|
+
LT = "<"
|
|
13
|
+
BETWEEN_INCLUDED = "BETWEEN_INCLUDED"
|
|
14
|
+
|
|
15
|
+
HAS = "has"
|
|
16
|
+
HAS_ANY = "has_any"
|
|
17
|
+
|
|
18
|
+
CUSTOM = "custom"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ValueT = TypeVar("ValueT")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SingleCondition(Generic[ValueT]):
|
|
26
|
+
field: str
|
|
27
|
+
operator: ConditionOperator
|
|
28
|
+
value: ValueT
|
|
29
|
+
|
|
30
|
+
def __and__(self, other: "Condition") -> "Condition":
|
|
31
|
+
return AndCondition(self, other)
|
|
32
|
+
|
|
33
|
+
def merge(self, other: "SingleCondition[Any]") -> "SingleCondition[Any]":
|
|
34
|
+
if other.field != self.field:
|
|
35
|
+
raise ValueError(f"Cannot merge conditions with different fields: {self.field} != {other.field}")
|
|
36
|
+
if self.operator is ConditionOperator.LE:
|
|
37
|
+
if other.operator is ConditionOperator.GE:
|
|
38
|
+
return SingleCondition(self.field, ConditionOperator.BETWEEN_INCLUDED, (other.value, self.value))
|
|
39
|
+
elif other.operator is ConditionOperator.LE:
|
|
40
|
+
return SingleCondition(self.field, ConditionOperator.LE, min(self.value, other.value))
|
|
41
|
+
elif self.operator is ConditionOperator.GE:
|
|
42
|
+
if other.operator is ConditionOperator.LE:
|
|
43
|
+
return other.merge(self)
|
|
44
|
+
elif other.operator is ConditionOperator.GE:
|
|
45
|
+
return SingleCondition(self.field, ConditionOperator.GE, max(self.value, other.value))
|
|
46
|
+
elif self.operator is ConditionOperator.LT:
|
|
47
|
+
if other.operator is ConditionOperator.LT:
|
|
48
|
+
return SingleCondition(self.field, ConditionOperator.LT, min(self.value, other.value))
|
|
49
|
+
elif self.operator is ConditionOperator.GT:
|
|
50
|
+
if other.operator is ConditionOperator.GT:
|
|
51
|
+
return SingleCondition(self.field, ConditionOperator.GT, max(self.value, other.value))
|
|
52
|
+
raise ValueError(f"Cannot merge condition with operator {self.operator} and {other.operator}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Condition = Union[SingleCondition, "AndCondition"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AndCondition:
|
|
59
|
+
def __init__(self, *conditions: Condition):
|
|
60
|
+
self.conditions: list[SingleCondition[Any]] = []
|
|
61
|
+
for c in conditions:
|
|
62
|
+
self.conditions.extend(self._unpack(c))
|
|
63
|
+
|
|
64
|
+
def _unpack(self, other: Condition) -> Sequence[SingleCondition]:
|
|
65
|
+
if isinstance(other, AndCondition):
|
|
66
|
+
return other.conditions
|
|
67
|
+
return [other]
|
|
68
|
+
|
|
69
|
+
def __and__(self, other: Condition) -> "AndCondition":
|
|
70
|
+
return AndCondition(*self.conditions, other)
|
|
71
|
+
|
|
72
|
+
def __iadd__(self, other):
|
|
73
|
+
self.conditions.extend(self._unpack(other))
|
|
74
|
+
|
|
75
|
+
def __repr__(self):
|
|
76
|
+
conditions = ", ".join(repr(c) for c in self.conditions)
|
|
77
|
+
return f"AndCondition({conditions})"
|
|
78
|
+
|
|
79
|
+
def __getitem__(self, item: str) -> SingleCondition[Any]:
|
|
80
|
+
return next(c for c in self.conditions if c.field == item)
|
|
81
|
+
|
|
82
|
+
def __contains__(self, item: str) -> bool:
|
|
83
|
+
return any(c.field == item for c in self.conditions)
|
|
84
|
+
|
|
85
|
+
def __len__(self) -> int:
|
|
86
|
+
return len(self.conditions)
|
|
87
|
+
|
|
88
|
+
def __iter__(self) -> Iterator[SingleCondition[Any]]:
|
|
89
|
+
return iter(self.conditions)
|
|
90
|
+
|
|
91
|
+
def find_all(self, item: str) -> Iterable[SingleCondition[Any]]:
|
|
92
|
+
for condition in self.conditions:
|
|
93
|
+
if condition.field == item:
|
|
94
|
+
yield condition
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Generic, Sequence, Callable, Optional, TypeVar, Any
|
|
4
|
+
|
|
5
|
+
from .condition import SingleCondition, ConditionOperator, AndCondition
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MatchField(str, Enum):
|
|
9
|
+
community = "community"
|
|
10
|
+
extcommunity = "extcommunity"
|
|
11
|
+
rd = "rd"
|
|
12
|
+
interface = "interface"
|
|
13
|
+
protocol = "protocol"
|
|
14
|
+
net_len = "net_len"
|
|
15
|
+
local_pref = "local_pref"
|
|
16
|
+
metric = "metric"
|
|
17
|
+
family = "family"
|
|
18
|
+
|
|
19
|
+
as_path_length = "as_path_length"
|
|
20
|
+
as_path_filter = "as_path_filter"
|
|
21
|
+
ipv6_prefix = "ipv6_prefix"
|
|
22
|
+
ip_prefix = "ip_prefix"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ValueT = TypeVar("ValueT")
|
|
26
|
+
_ConditionMethod = Callable[["ConditionFactory[ValueT]", ValueT], SingleCondition[ValueT]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def condition_method(operator: ConditionOperator) -> _ConditionMethod:
|
|
30
|
+
def method(self: "ConditionFactory[ValueT]", other: ValueT) -> SingleCondition[ValueT]:
|
|
31
|
+
if operator.value not in self.supported_ops:
|
|
32
|
+
raise NotImplementedError(f"Operator {operator.value} is not supported for field {self.field}")
|
|
33
|
+
return SingleCondition(self.field, operator, other)
|
|
34
|
+
|
|
35
|
+
method.__name__ = operator.value
|
|
36
|
+
return method
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConditionFactory(Generic[ValueT]):
|
|
40
|
+
def __init__(self, field: str, supported_ops: list[str]):
|
|
41
|
+
self.field = field
|
|
42
|
+
self.supported_ops = supported_ops
|
|
43
|
+
|
|
44
|
+
# https://github.com/python/typeshed/issues/3685
|
|
45
|
+
eq = __eq__ = condition_method(ConditionOperator.EQ) # type: ignore[assignment]
|
|
46
|
+
gt = __gt__ = condition_method(ConditionOperator.GT)
|
|
47
|
+
ge = __ge__ = condition_method(ConditionOperator.GE)
|
|
48
|
+
lt = __lt__ = condition_method(ConditionOperator.LT)
|
|
49
|
+
le = __le__ = condition_method(ConditionOperator.LE)
|
|
50
|
+
between_included = condition_method(ConditionOperator.BETWEEN_INCLUDED)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SetConditionFactory(Generic[ValueT]):
|
|
54
|
+
def __init__(self, field: str) -> None:
|
|
55
|
+
self.field = field
|
|
56
|
+
|
|
57
|
+
def has(self, *values: ValueT) -> SingleCondition[Sequence[ValueT]]:
|
|
58
|
+
return SingleCondition(self.field, ConditionOperator.HAS, values)
|
|
59
|
+
|
|
60
|
+
def has_any(self, *values: ValueT) -> SingleCondition[Sequence[ValueT]]:
|
|
61
|
+
return SingleCondition(self.field, ConditionOperator.HAS_ANY, values)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class PrefixMatchValue:
|
|
66
|
+
names: Sequence[str]
|
|
67
|
+
or_longer: Optional[tuple[int, int]] # ????
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Checkable:
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.community = SetConditionFactory[str](MatchField.community)
|
|
73
|
+
self.extcommunity = SetConditionFactory[str](MatchField.extcommunity)
|
|
74
|
+
self.rd = SetConditionFactory[str](MatchField.rd)
|
|
75
|
+
self.interface = ConditionFactory[str](MatchField.interface, ["=="])
|
|
76
|
+
self.protocol = ConditionFactory[str](MatchField.protocol, ["=="])
|
|
77
|
+
self.net_len = ConditionFactory[int](MatchField.net_len, ["==", "!="])
|
|
78
|
+
self.local_pref = ConditionFactory[int](MatchField.local_pref, ["<"])
|
|
79
|
+
self.metric = ConditionFactory[int](MatchField.metric, ["=="])
|
|
80
|
+
self.family = ConditionFactory[int](MatchField.family, ["=="])
|
|
81
|
+
self.as_path_length = ConditionFactory[int](MatchField.as_path_length, ["==", ">=", "<=", "BETWEEN_INCLUDED"])
|
|
82
|
+
|
|
83
|
+
def as_path_filter(self, name: str) -> SingleCondition[str]:
|
|
84
|
+
return SingleCondition(MatchField.as_path_filter, ConditionOperator.EQ, name)
|
|
85
|
+
|
|
86
|
+
def match_v6(self, *names: str, or_longer: Optional[tuple[int, int]] = None) -> SingleCondition[PrefixMatchValue]:
|
|
87
|
+
return SingleCondition(MatchField.ipv6_prefix, ConditionOperator.CUSTOM, PrefixMatchValue(names, or_longer))
|
|
88
|
+
|
|
89
|
+
def match_v4(self, *names: str, or_longer: Optional[tuple[int, int]] = None) -> SingleCondition[PrefixMatchValue]:
|
|
90
|
+
return SingleCondition(MatchField.ip_prefix, ConditionOperator.CUSTOM, PrefixMatchValue(names, or_longer))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def merge_conditions(and_condition: AndCondition) -> AndCondition:
|
|
94
|
+
conditions: dict[str, SingleCondition[Any]] = {}
|
|
95
|
+
for condition in and_condition.conditions:
|
|
96
|
+
if condition.field in conditions:
|
|
97
|
+
conditions[condition.field] = conditions[condition.field].merge(condition)
|
|
98
|
+
else:
|
|
99
|
+
conditions[condition.field] = condition
|
|
100
|
+
return AndCondition(*conditions.values())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
R = Checkable()
|
annet/rpl/policy.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .action import Action
|
|
6
|
+
from .condition import AndCondition
|
|
7
|
+
from .result import ResultType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class RoutingPolicyStatement:
|
|
12
|
+
name: Optional[str]
|
|
13
|
+
number: Optional[int]
|
|
14
|
+
match: AndCondition
|
|
15
|
+
then: Action
|
|
16
|
+
result: ResultType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RoutingPolicy:
|
|
21
|
+
name: str
|
|
22
|
+
statements: Sequence[RoutingPolicyStatement]
|
annet/rpl/result.py
ADDED
annet/rpl/routemap.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Callable, Generic, TypeVar, Union
|
|
3
|
+
|
|
4
|
+
from .action import Action
|
|
5
|
+
from .condition import AndCondition, Condition
|
|
6
|
+
from .match_builder import merge_conditions
|
|
7
|
+
from .policy import RoutingPolicy, RoutingPolicyStatement
|
|
8
|
+
from .result import ResultType
|
|
9
|
+
from .statement_builder import StatementBuilder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Route:
|
|
13
|
+
def __init__(self, name: str):
|
|
14
|
+
self.name = name
|
|
15
|
+
self.statements: list[RoutingPolicyStatement] = []
|
|
16
|
+
|
|
17
|
+
def __call__(
|
|
18
|
+
self,
|
|
19
|
+
*conditions: Condition,
|
|
20
|
+
name: Optional[str] = None,
|
|
21
|
+
number: Optional[int] = None,
|
|
22
|
+
) -> "StatementBuilder":
|
|
23
|
+
statement = RoutingPolicyStatement(
|
|
24
|
+
name=name,
|
|
25
|
+
number=number,
|
|
26
|
+
match=merge_conditions(AndCondition(*conditions)),
|
|
27
|
+
then=Action(),
|
|
28
|
+
result=ResultType.NEXT,
|
|
29
|
+
)
|
|
30
|
+
self.statements.append(statement)
|
|
31
|
+
return StatementBuilder(statement=statement)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DeviceT = TypeVar("DeviceT")
|
|
35
|
+
RouteHandlerFunc = Callable[[DeviceT, Route], None]
|
|
36
|
+
Decorator = Callable[[RouteHandlerFunc[DeviceT]], RouteHandlerFunc[DeviceT]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Handler(Generic[DeviceT]):
|
|
41
|
+
name: str
|
|
42
|
+
func: RouteHandlerFunc[DeviceT]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RouteMap(Generic[DeviceT]):
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self.handlers: list[Handler[DeviceT]] = []
|
|
48
|
+
self.submaps: list[RouteMap[DeviceT]] = []
|
|
49
|
+
|
|
50
|
+
def __call__(
|
|
51
|
+
self, func: Optional[RouteHandlerFunc[DeviceT]] = None, *, name: str = "",
|
|
52
|
+
) -> Union[RouteHandlerFunc[DeviceT], Decorator[DeviceT]]:
|
|
53
|
+
def decorator(func: RouteHandlerFunc[DeviceT]) -> RouteHandlerFunc[DeviceT]:
|
|
54
|
+
nonlocal name
|
|
55
|
+
if not name:
|
|
56
|
+
name = func.__name__
|
|
57
|
+
self.handlers.append(Handler(name, func))
|
|
58
|
+
return func
|
|
59
|
+
|
|
60
|
+
if func is None:
|
|
61
|
+
return decorator
|
|
62
|
+
return decorator(func)
|
|
63
|
+
|
|
64
|
+
def include(self, other: "RouteMap[DeviceT]") -> None:
|
|
65
|
+
self.submaps.append(other)
|
|
66
|
+
|
|
67
|
+
def apply(self, device: DeviceT) -> list[RoutingPolicy]:
|
|
68
|
+
result: list[RoutingPolicy] = []
|
|
69
|
+
|
|
70
|
+
for handler in self.handlers:
|
|
71
|
+
route = Route(handler.name)
|
|
72
|
+
handler.func(device, route)
|
|
73
|
+
result.append(RoutingPolicy(route.name, route.statements))
|
|
74
|
+
for submap in self.submaps:
|
|
75
|
+
result.extend(submap.apply(device))
|
|
76
|
+
return result
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional, Literal, TypeVar, Union, Any
|
|
6
|
+
|
|
7
|
+
from .action import SingleAction, ActionType
|
|
8
|
+
from .policy import RoutingPolicyStatement
|
|
9
|
+
from .result import ResultType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThenField(str, Enum):
|
|
13
|
+
community = "community"
|
|
14
|
+
extcommunity = "extcommunity"
|
|
15
|
+
as_path = "as_path"
|
|
16
|
+
local_pref = "local_pref"
|
|
17
|
+
metric = "metric"
|
|
18
|
+
rpki_valid_state = "rpki_valid_state"
|
|
19
|
+
resolution = "resolution"
|
|
20
|
+
mpls_label = "mpls_label"
|
|
21
|
+
metric_type = "metric_type"
|
|
22
|
+
origin = "origin"
|
|
23
|
+
tag = "tag"
|
|
24
|
+
|
|
25
|
+
next_hop = "next_hop"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ValueT = TypeVar("ValueT")
|
|
29
|
+
_Setter = Callable[[ValueT], SingleAction[ValueT]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CommunityActionValue:
|
|
34
|
+
replaced: Optional[list[str]] = None # None means no replacement is done
|
|
35
|
+
added: list[str] = field(default_factory=list)
|
|
36
|
+
removed: list[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
def __bool__(self) -> bool: # check if any action required
|
|
39
|
+
return bool(self.replaced is not None or self.added or self.removed)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CommunityActionBuilder:
|
|
43
|
+
def __init__(self, community: CommunityActionValue):
|
|
44
|
+
self._community = community
|
|
45
|
+
|
|
46
|
+
def add(self, *community: str) -> None:
|
|
47
|
+
for c in community:
|
|
48
|
+
self._community.added.append(c)
|
|
49
|
+
|
|
50
|
+
def remove(self, *community: str) -> None:
|
|
51
|
+
for c in community:
|
|
52
|
+
self._community.removed.append(c)
|
|
53
|
+
|
|
54
|
+
def set(self, *community: str) -> None:
|
|
55
|
+
self._community.added.clear()
|
|
56
|
+
self._community.removed.clear()
|
|
57
|
+
self._community.replaced = list(community)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class AsPathActionValue:
|
|
62
|
+
set: Optional[list[str]] = None # None means no replacement is done
|
|
63
|
+
prepend: list[str] = field(default_factory=list)
|
|
64
|
+
expand: list[str] = field(default_factory=list)
|
|
65
|
+
expand_last_as: str = ""
|
|
66
|
+
delete: list[str] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
def __bool__(self) -> bool: # check if any action required
|
|
69
|
+
return bool(
|
|
70
|
+
self.set is not None or self.prepend or self.expand or self.expand_last_as or self.delete
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
RawAsNum = Union[str, int]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AsPathActionBuilder:
|
|
78
|
+
def __init__(self, as_path_value: AsPathActionValue):
|
|
79
|
+
self._as_path_value = as_path_value
|
|
80
|
+
|
|
81
|
+
def prepend(self, *values: RawAsNum) -> None:
|
|
82
|
+
self._as_path_value.prepend = list(map(str, values))
|
|
83
|
+
|
|
84
|
+
def delete(self, *values: RawAsNum) -> None:
|
|
85
|
+
self._as_path_value.delete = list(map(str, values))
|
|
86
|
+
|
|
87
|
+
def expand(self, *values: RawAsNum) -> None:
|
|
88
|
+
self._as_path_value.expand = list(map(str, values))
|
|
89
|
+
|
|
90
|
+
def expand_last_as(self, value: RawAsNum) -> None:
|
|
91
|
+
self._as_path_value.expand_last_as = str(value)
|
|
92
|
+
|
|
93
|
+
def set(self, *values: RawAsNum) -> None:
|
|
94
|
+
self._as_path_value.expand.clear()
|
|
95
|
+
self._as_path_value.expand_last_as = ""
|
|
96
|
+
self._as_path_value.delete.clear()
|
|
97
|
+
self._as_path_value.prepend.clear()
|
|
98
|
+
self._as_path_value.set = list(map(str, values))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class NextHopActionValue:
|
|
103
|
+
target: Optional[Literal["self", "discard", "peer", "ipv4_addr", "ipv6_addr", "mapped_ipv4"]] = None
|
|
104
|
+
addr: str = ""
|
|
105
|
+
|
|
106
|
+
def __bool__(self) -> bool:
|
|
107
|
+
return bool(self.target)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class NextHopActionBuilder:
|
|
111
|
+
def __init__(self, next_hop_value: NextHopActionValue):
|
|
112
|
+
self._next_hop_value = next_hop_value
|
|
113
|
+
|
|
114
|
+
def ipv4_addr(self, value: str) -> None:
|
|
115
|
+
self._next_hop_value.target = "ipv4_addr"
|
|
116
|
+
self._next_hop_value.addr = value
|
|
117
|
+
|
|
118
|
+
def ipv6_addr(self, value: str) -> None:
|
|
119
|
+
self._next_hop_value.target = "ipv6_addr"
|
|
120
|
+
self._next_hop_value.addr = value
|
|
121
|
+
|
|
122
|
+
def mapped_ipv4(self, value: str) -> None:
|
|
123
|
+
self._next_hop_value.target = "mapped_ipv4"
|
|
124
|
+
self._next_hop_value.addr = value
|
|
125
|
+
|
|
126
|
+
def self(self) -> None:
|
|
127
|
+
self._next_hop_value.target = "self"
|
|
128
|
+
self._next_hop_value.addr = ""
|
|
129
|
+
|
|
130
|
+
def peer(self) -> None:
|
|
131
|
+
self._next_hop_value.target = "peer"
|
|
132
|
+
self._next_hop_value.addr = ""
|
|
133
|
+
|
|
134
|
+
def discard(self) -> None:
|
|
135
|
+
self._next_hop_value.target = "discard"
|
|
136
|
+
self._next_hop_value.addr = ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class StatementBuilder:
|
|
140
|
+
def __init__(self, statement: RoutingPolicyStatement) -> None:
|
|
141
|
+
self._statement = statement
|
|
142
|
+
self._added_as_path: list[int] = []
|
|
143
|
+
self._community = CommunityActionValue()
|
|
144
|
+
self._extcommunity = CommunityActionValue()
|
|
145
|
+
self._as_path = AsPathActionValue()
|
|
146
|
+
self._next_hop = NextHopActionValue()
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def next_hop(self) -> NextHopActionBuilder:
|
|
150
|
+
return NextHopActionBuilder(self._next_hop)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def as_path(self) -> AsPathActionBuilder:
|
|
154
|
+
return AsPathActionBuilder(self._as_path)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def community(self) -> CommunityActionBuilder:
|
|
158
|
+
return CommunityActionBuilder(self._community)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def extcommunity(self) -> CommunityActionBuilder:
|
|
162
|
+
return CommunityActionBuilder(self._extcommunity)
|
|
163
|
+
|
|
164
|
+
def _set(self, field: str, value: ValueT) -> None:
|
|
165
|
+
action = self._statement.then
|
|
166
|
+
if field in action:
|
|
167
|
+
action[field].type = ActionType.SET
|
|
168
|
+
action[field].value = value
|
|
169
|
+
else:
|
|
170
|
+
action.append(SingleAction(
|
|
171
|
+
field=field,
|
|
172
|
+
type=ActionType.SET,
|
|
173
|
+
value=value,
|
|
174
|
+
))
|
|
175
|
+
|
|
176
|
+
def set_local_pref(self, value: int) -> None:
|
|
177
|
+
self._set(ThenField.local_pref, value)
|
|
178
|
+
|
|
179
|
+
def set_metric_type(self, value: str) -> None:
|
|
180
|
+
self._set(ThenField.metric_type, value)
|
|
181
|
+
|
|
182
|
+
def set_metric(self, value: int) -> None:
|
|
183
|
+
self._set(ThenField.metric, value)
|
|
184
|
+
|
|
185
|
+
def add_metric(self, value: int) -> None:
|
|
186
|
+
action = self._statement.then
|
|
187
|
+
field = ThenField.metric
|
|
188
|
+
if field in action:
|
|
189
|
+
old_action = action[field]
|
|
190
|
+
if old_action.type == ActionType.SET:
|
|
191
|
+
action[field].value += value
|
|
192
|
+
elif old_action.type == ActionType.ADD:
|
|
193
|
+
action[field].value = value
|
|
194
|
+
else:
|
|
195
|
+
raise RuntimeError(f"Unknown action type {old_action.type} for metric")
|
|
196
|
+
else:
|
|
197
|
+
action.append(SingleAction(
|
|
198
|
+
field=field,
|
|
199
|
+
type=ActionType.ADD,
|
|
200
|
+
value=value,
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
def set_rpki_valid_state(self, value: str) -> None:
|
|
204
|
+
self._set(ThenField.rpki_valid_state, value)
|
|
205
|
+
|
|
206
|
+
def set_resolution(self, value: str) -> None:
|
|
207
|
+
self._set(ThenField.resolution, value)
|
|
208
|
+
|
|
209
|
+
def set_mpls_label(self) -> None:
|
|
210
|
+
self._set(ThenField.mpls_label, True)
|
|
211
|
+
|
|
212
|
+
def set_origin(self, value: str) -> None:
|
|
213
|
+
self._set(ThenField.origin, value)
|
|
214
|
+
|
|
215
|
+
def set_tag(self, value: int) -> None:
|
|
216
|
+
self._set(ThenField.tag, value)
|
|
217
|
+
|
|
218
|
+
def set_next_hop(self, value: Literal["self", "peer"]) -> None: # ???
|
|
219
|
+
self._set(ThenField.next_hop, value)
|
|
220
|
+
|
|
221
|
+
def __enter__(self) -> "StatementBuilder":
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
225
|
+
if self._community:
|
|
226
|
+
self._statement.then.append(SingleAction(
|
|
227
|
+
field=ThenField.community,
|
|
228
|
+
type=ActionType.CUSTOM,
|
|
229
|
+
value=self._community,
|
|
230
|
+
))
|
|
231
|
+
if self._extcommunity:
|
|
232
|
+
self._statement.then.append(SingleAction(
|
|
233
|
+
field=ThenField.extcommunity,
|
|
234
|
+
type=ActionType.CUSTOM,
|
|
235
|
+
value=self._extcommunity,
|
|
236
|
+
))
|
|
237
|
+
if self._as_path:
|
|
238
|
+
self._statement.then.append(SingleAction(
|
|
239
|
+
field=ThenField.as_path,
|
|
240
|
+
type=ActionType.CUSTOM,
|
|
241
|
+
value=self._as_path,
|
|
242
|
+
))
|
|
243
|
+
if self._next_hop:
|
|
244
|
+
self._statement.then.append(SingleAction(
|
|
245
|
+
field=ThenField.next_hop,
|
|
246
|
+
type=ActionType.CUSTOM,
|
|
247
|
+
value=self._next_hop,
|
|
248
|
+
))
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def allow(self) -> None:
|
|
252
|
+
self._statement.result = ResultType.ALLOW
|
|
253
|
+
|
|
254
|
+
def deny(self) -> None:
|
|
255
|
+
self._statement.result = ResultType.DENY
|
|
256
|
+
|
|
257
|
+
def next(self) -> None:
|
|
258
|
+
self._statement.result = ResultType.NEXT
|
|
259
|
+
|
|
260
|
+
def next_policy(self) -> None:
|
|
261
|
+
self._statement.result = ResultType.NEXT_POLICY
|
|
262
|
+
|
|
263
|
+
def add_as_path(self, *as_path: int) -> None:
|
|
264
|
+
self._added_as_path.extend(as_path)
|
|
265
|
+
|
|
266
|
+
def custom_action(self, action: SingleAction[Any]) -> None:
|
|
267
|
+
self._statement.then.append(action)
|
annet/rulebook/cisco/misc.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from typing import Any
|
|
2
4
|
|
|
3
5
|
from annet.annlib.types import Op
|
|
4
6
|
|
|
@@ -55,3 +57,93 @@ def banner_login(rule, key, diff, **_):
|
|
|
55
57
|
yield (False, f"banner login {key}", None)
|
|
56
58
|
else:
|
|
57
59
|
yield from common.default(rule, key, diff)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bgp_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
|
|
63
|
+
"""
|
|
64
|
+
Some oder versions of Cisco IOS doesn't create subsection for address family block.
|
|
65
|
+
|
|
66
|
+
it looks like:
|
|
67
|
+
|
|
68
|
+
router bgp 65111
|
|
69
|
+
bgp router-id 1.1.1.1
|
|
70
|
+
bgp log-neighbor-changes
|
|
71
|
+
neighbor SPINE peer-group
|
|
72
|
+
!
|
|
73
|
+
address-family ipv4
|
|
74
|
+
neighbor SPINE send-community both
|
|
75
|
+
neighbor SPINE soft-reconfiguration inbound
|
|
76
|
+
neighbor SPINE route-map TOR_IMPORT_SPINE in
|
|
77
|
+
neighbor SPINE route-map TOR_EXPORT_SPINE out
|
|
78
|
+
exit-address-family
|
|
79
|
+
|
|
80
|
+
but should be
|
|
81
|
+
|
|
82
|
+
router bgp 65111
|
|
83
|
+
bgp router-id 1.1.1.1
|
|
84
|
+
bgp log-neighbor-changes
|
|
85
|
+
neighbor SPINE peer-group
|
|
86
|
+
!
|
|
87
|
+
address-family ipv4
|
|
88
|
+
neighbor SPINE send-community both
|
|
89
|
+
neighbor SPINE soft-reconfiguration inbound
|
|
90
|
+
neighbor SPINE route-map TOR_IMPORT_SPINE in
|
|
91
|
+
neighbor SPINE route-map TOR_EXPORT_SPINE out
|
|
92
|
+
exit-address-family
|
|
93
|
+
|
|
94
|
+
The diff_logic func do it before make diff.
|
|
95
|
+
"""
|
|
96
|
+
corrected_old = _create_subsections(old, "address-family")
|
|
97
|
+
|
|
98
|
+
yield from common.default_diff(corrected_old, new, diff_pre, _pops)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _create_subsections(data: OrderedDict[str, Any], sub_section_prefix: str) -> OrderedDict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Reorganizes the given OrderedDict to nest commands under their respective
|
|
104
|
+
sub_section_prefix keys.
|
|
105
|
+
|
|
106
|
+
This function traverses the entries in the provided OrderedDict and groups
|
|
107
|
+
together all entries that are between keys with sub_section_prefix under those
|
|
108
|
+
keys as nested OrderedDicts. The reorganization keeps the order of entries
|
|
109
|
+
stable, only adding nesting where appropriate.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
data (OrderedDict): The original configuration to be transformed.
|
|
113
|
+
sub_section_prefix (str): Prefix of subsection key
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
OrderedDict: A new OrderedDict with nested 'address-family' sections.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
result = OrderedDict()
|
|
120
|
+
sub_section = None
|
|
121
|
+
temp: OrderedDict = OrderedDict()
|
|
122
|
+
|
|
123
|
+
for key, value in data.items():
|
|
124
|
+
# make nested loop if found nested values
|
|
125
|
+
if value:
|
|
126
|
+
fixed_value: OrderedDict[str, Any] = _create_subsections(value, sub_section_prefix)
|
|
127
|
+
else:
|
|
128
|
+
fixed_value = value
|
|
129
|
+
if key.startswith(sub_section_prefix):
|
|
130
|
+
# in case of data has already had subsections
|
|
131
|
+
if value:
|
|
132
|
+
result[key] = fixed_value
|
|
133
|
+
continue
|
|
134
|
+
# if previous subsection present save collected data from temporary dict
|
|
135
|
+
if sub_section:
|
|
136
|
+
result[sub_section] = temp
|
|
137
|
+
# find a new subsection and initialize new dict
|
|
138
|
+
sub_section = key
|
|
139
|
+
temp = OrderedDict()
|
|
140
|
+
# put found data to temporary dict
|
|
141
|
+
elif sub_section:
|
|
142
|
+
temp[key] = fixed_value
|
|
143
|
+
else:
|
|
144
|
+
result[key] = fixed_value
|
|
145
|
+
# if data is finished save collected data from temporary dict
|
|
146
|
+
if sub_section:
|
|
147
|
+
result[sub_section] = temp
|
|
148
|
+
|
|
149
|
+
return result
|
annet/rulebook/texts/arista.rul
CHANGED
annet/rulebook/texts/cisco.rul
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: annet
|
|
3
|
+
Version: 0.16.18
|
|
4
|
+
Summary: annet
|
|
5
|
+
Home-page: https://github.com/annetutil/annet
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: AUTHORS
|
|
10
|
+
Requires-Dist: colorama>=0.4.6
|
|
11
|
+
Requires-Dist: tabulate>=0.9.0
|
|
12
|
+
Requires-Dist: jsonpatch>=1.33
|
|
13
|
+
Requires-Dist: jsonpointer>=2.4
|
|
14
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
15
|
+
Requires-Dist: Pygments>=2.14.0
|
|
16
|
+
Requires-Dist: Mako>=1.2.4
|
|
17
|
+
Requires-Dist: Jinja2>=3.1.2
|
|
18
|
+
Requires-Dist: psutil>=5.8.0
|
|
19
|
+
Requires-Dist: packaging>=23.2
|
|
20
|
+
Requires-Dist: contextlog>=1.1
|
|
21
|
+
Requires-Dist: valkit>=0.1.4
|
|
22
|
+
Requires-Dist: yarl>=1.8.2
|
|
23
|
+
Requires-Dist: adaptix==3.0.0b7
|
|
24
|
+
Requires-Dist: dataclass-rest==0.4
|
|
25
|
+
Provides-Extra: netbox
|
|
26
|
+
Requires-Dist: annetbox[sync]>=0.1.8; extra == "netbox"
|
|
@@ -68,7 +68,7 @@ annet/annlib/rbparser/syntax.py,sha256=iZ7Y-4QQBw4L3UtjEh54qisiRDhobl7HZxFNdP8mi
|
|
|
68
68
|
annet/annlib/rulebook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
69
69
|
annet/annlib/rulebook/common.py,sha256=Kd9Xout0xC6ZZDnyaORx0W-1kSM-gTgjQbp1iIXWxic,16489
|
|
70
70
|
annet/api/__init__.py,sha256=7EB3d89kGXKf4Huw7cqyOULTGC9ydL4sHiVw8K0Aeqg,33722
|
|
71
|
-
annet/configs/context.yml,sha256=
|
|
71
|
+
annet/configs/context.yml,sha256=RVLrKLIHpCty7AGwOnmqf7Uu0iZQCn-AjYhophDJer8,259
|
|
72
72
|
annet/configs/logging.yaml,sha256=EUagfir99QqA73Scc3k7sfQccbU3E1SvEQdyhLFtCl4,997
|
|
73
73
|
annet/generators/__init__.py,sha256=rVHHDTPKHPZsml1eNEAj3o-8RweFTN8J7LX3tKMXdIY,16402
|
|
74
74
|
annet/generators/base.py,sha256=rgQLcQBPZm4ecbKmRhVOpBR-GFJAiVfdb_y5f2-LUR8,3670
|
|
@@ -89,6 +89,14 @@ annet/mesh/match_args.py,sha256=CR3kdIV9NGtyk9E2JbcOQ3TRuYEryTWP3m2yCo2VCWg,5751
|
|
|
89
89
|
annet/mesh/models_converter.py,sha256=3q2zs7K8S3pfYSUKKRdtl5CJGbeg4TtYxofAVs_MBsk,3085
|
|
90
90
|
annet/mesh/peer_models.py,sha256=xyLbWtwJ5Y4OLf-OVZeHwRn2qyWhipOvhD4pDHTbvTE,2665
|
|
91
91
|
annet/mesh/registry.py,sha256=G-FszWc_VKeN3eVpb-MRGbAGzbcSuEVAzbDC2k747XA,8611
|
|
92
|
+
annet/rpl/__init__.py,sha256=0kcIktE3AmS0rlm9xzVDf53xk08OeZXgD-6ZLCt_KCs,731
|
|
93
|
+
annet/rpl/action.py,sha256=PY6W66j908RuqQ1_ioxayqVN-70rxDk5Z59EGHtxI98,1246
|
|
94
|
+
annet/rpl/condition.py,sha256=MJri4MbWtPkLHIsLMAtsIEF7e8IAS9dIImjmJs5vS5U,3418
|
|
95
|
+
annet/rpl/match_builder.py,sha256=6uutlI9TU75ClpcKZx0t8pqYxr20vr0h9itBrtrm_tQ,4194
|
|
96
|
+
annet/rpl/policy.py,sha256=P1Kt-8fHFxEczeP-RwkK_wrGN0p7IR-hOApEd2vC55E,448
|
|
97
|
+
annet/rpl/result.py,sha256=PHFn1zhDeqLBn07nkYw5vsoXew4nTwkklOwqvFWzBLg,141
|
|
98
|
+
annet/rpl/routemap.py,sha256=ZxYSRXWtk3CG5OQuVVVYePpPP9Zbwk-ajWvQ0wqWP_8,2422
|
|
99
|
+
annet/rpl/statement_builder.py,sha256=dSYrLosInUb4ewHzX4PeEzbRlIaz6Df4N1EytIJGccQ,8442
|
|
92
100
|
annet/rulebook/__init__.py,sha256=14IpOfTbeJtre7JKrfXVYiH0qAXsUSOL7AatUFmSQs0,3847
|
|
93
101
|
annet/rulebook/common.py,sha256=zK1s2c5lc5HQbIlMUQ4HARQudXSgOYiZ_Sxc2I_tHqg,721
|
|
94
102
|
annet/rulebook/deploying.py,sha256=XV0XQvc3YvwM8SOgOQlc-fCW4bnjQg_1CTZkTwMp14A,2972
|
|
@@ -102,7 +110,7 @@ annet/rulebook/b4com/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
102
110
|
annet/rulebook/b4com/file.py,sha256=zK7RwBk1YaVoDSFSg1u7Pt8u0Fk3nhhu27aJRngemwc,137
|
|
103
111
|
annet/rulebook/cisco/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
104
112
|
annet/rulebook/cisco/iface.py,sha256=WISkzjp_G7WKPpg098FCIm4b7ipOxtRLOQbu-7gMUL0,1792
|
|
105
|
-
annet/rulebook/cisco/misc.py,sha256=
|
|
113
|
+
annet/rulebook/cisco/misc.py,sha256=KBnxBw5vs-GSoldMBzEB8ygyHEntiydn1vchZDBA3No,5286
|
|
106
114
|
annet/rulebook/cisco/vlandb.py,sha256=pdQ0Ca976_5_cNBbTI6ADN1SP8aAngVBs1AZWltmpsw,3319
|
|
107
115
|
annet/rulebook/huawei/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
108
116
|
annet/rulebook/huawei/aaa.py,sha256=Xi8nWyBFaUz4SgoN1NQeOcXzBGCfINQDNiC-crq08uA,3445
|
|
@@ -118,7 +126,7 @@ annet/rulebook/routeros/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
|
118
126
|
annet/rulebook/routeros/file.py,sha256=zK7RwBk1YaVoDSFSg1u7Pt8u0Fk3nhhu27aJRngemwc,137
|
|
119
127
|
annet/rulebook/texts/arista.deploy,sha256=OS9eFyJpEPztcOHkBwajw_RTJfTT7ivaMHfx4_HXaUg,792
|
|
120
128
|
annet/rulebook/texts/arista.order,sha256=TKy3S56tIypwfVaw0akK1sXGatCpLVwpB4Jvq-dIu90,1457
|
|
121
|
-
annet/rulebook/texts/arista.rul,sha256=
|
|
129
|
+
annet/rulebook/texts/arista.rul,sha256=QQbro8eFlc7DCHk0-CTHX_rnj5rqumRzXlY60ga72jo,815
|
|
122
130
|
annet/rulebook/texts/aruba.deploy,sha256=hI432Bq-of_LMXuUflCu7eNSEFpx6qmj0KItEw6sgHI,841
|
|
123
131
|
annet/rulebook/texts/aruba.order,sha256=ZMakkn0EJ9zomgY6VssoptJImrHrUmYnCqivzLBFTRo,1158
|
|
124
132
|
annet/rulebook/texts/aruba.rul,sha256=zvGVpoYyJvMoL0fb1NQ8we_GCLZXno8nwWpZIOScLQQ,2584
|
|
@@ -127,7 +135,7 @@ annet/rulebook/texts/b4com.order,sha256=G3aToAIHHzKzDCM3q7_lyr9wJvuVOXVbVvF3wm5P
|
|
|
127
135
|
annet/rulebook/texts/b4com.rul,sha256=5mqyUg_oLRSny2iH6QdhfDWVu6kzgDURtlSATD7DFno,1056
|
|
128
136
|
annet/rulebook/texts/cisco.deploy,sha256=XvXWeOMahE8Uc9RF0xkJj8jGknD4vit8H_f24ubPX7w,1226
|
|
129
137
|
annet/rulebook/texts/cisco.order,sha256=OvNHMNqkCc-DN2dEjLCTKv_7ZhiaHt4q2X4Y4Z8dvR4,1901
|
|
130
|
-
annet/rulebook/texts/cisco.rul,sha256=
|
|
138
|
+
annet/rulebook/texts/cisco.rul,sha256=xMJJgTGmg5wZXTIp5SUdLJIgwka7AIv9Uj6-DWBV3UQ,3055
|
|
131
139
|
annet/rulebook/texts/huawei.deploy,sha256=azEC6_jQRzwnTSrNgag0hHh6L7hezS_eMk6ZDZfWyXI,10444
|
|
132
140
|
annet/rulebook/texts/huawei.order,sha256=ENllPX4kO6xNw2mUQcx11yhxo3tKStZ5mUyc0C6s3d0,10657
|
|
133
141
|
annet/rulebook/texts/huawei.rul,sha256=02Fi1RG4YYea2clHCluBuJDKNbT0hS9jtsk6_h6GK8k,12958
|
|
@@ -152,10 +160,14 @@ annet_generators/example/lldp.py,sha256=24bGvShxbio-JxUdaehyPRu31LhH9YwSwFDrWVRn
|
|
|
152
160
|
annet_generators/mesh_example/__init__.py,sha256=NfNWgXn1TNiWI6A5tmU6Y-4QV2i33td0Qs3re0MNNMo,218
|
|
153
161
|
annet_generators/mesh_example/bgp.py,sha256=jzyDndSSGYyYBquDnLlR-7P5lzmUKcSyYCml3VsoMC0,1385
|
|
154
162
|
annet_generators/mesh_example/mesh_logic.py,sha256=DJS5JMCTs0rs0LN__0LulNgo2ekUcWiOMe02BlOeFas,1454
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
annet-0.16.
|
|
160
|
-
annet-0.16.
|
|
161
|
-
annet-0.16.
|
|
163
|
+
annet_generators/rpl_example/__init__.py,sha256=z4-gsDv06BBpgTwRohc50VBQYFD26QVu8Zr8Ngi_iqY,244
|
|
164
|
+
annet_generators/rpl_example/items.py,sha256=6x7b0wZ7Vjn6yCaJ-aGbpTHm7fyqO77b-LRqzzhEbh4,615
|
|
165
|
+
annet_generators/rpl_example/policy_generator.py,sha256=KFCqn347CIPcnllOHfINYeKgNSr6Wl-bdM5Xj_YKhYM,11183
|
|
166
|
+
annet_generators/rpl_example/route_policy.py,sha256=QjxFjkePHfTo2CpMeRVaDqZXNXLM-gGlE8EocHuOR4Y,1189
|
|
167
|
+
annet-0.16.18.dist-info/AUTHORS,sha256=rh3w5P6gEgqmuC-bw-HB68vBCr-yIBFhVL0PG4hguLs,878
|
|
168
|
+
annet-0.16.18.dist-info/LICENSE,sha256=yPxl7dno02Pw7gAcFPIFONzx_gapwDoPXsIsh6Y7lC0,1079
|
|
169
|
+
annet-0.16.18.dist-info/METADATA,sha256=pB-87I1B1EvDeahyJFJD6j2DxvPEBYfWT44e8D0a-8U,728
|
|
170
|
+
annet-0.16.18.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
171
|
+
annet-0.16.18.dist-info/entry_points.txt,sha256=5lIaDGlGi3l6QQ2ry2jZaqViP5Lvt8AmsegdD0Uznck,192
|
|
172
|
+
annet-0.16.18.dist-info/top_level.txt,sha256=QsoTZBsUtwp_FEcmRwuN8QITBmLOZFqjssRfKilGbP8,23
|
|
173
|
+
annet-0.16.18.dist-info/RECORD,,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
AS_PATH_FILTERS = {
|
|
5
|
+
"ASP_EXAMPLE": [".*123456.*"],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
IPV6_PREFIX_LISTS = {
|
|
9
|
+
"IPV6_LIST_EXAMPLE": ["2a13:5941::/32"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Community:
|
|
15
|
+
values: Sequence[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ExtCommunity:
|
|
20
|
+
values: Sequence[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
COMMUNITIES = {
|
|
24
|
+
"COMMUNITY_EXAMPLE_ADD": Community(["1234:1000"]),
|
|
25
|
+
"COMMUNITY_EXAMPLE_REMOVE": Community(["12345:999"]),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
EXT_COMMUNITIES = {
|
|
29
|
+
"COMMUNITY_EXAMPLE_ADD": ExtCommunity(["1234:1000"]),
|
|
30
|
+
"COMMUNITY_EXAMPLE_REMOVE": ExtCommunity(["12345:999"]),
|
|
31
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from collections.abc import Iterator, Sequence
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from annet.generators import PartialGenerator, BaseGenerator
|
|
5
|
+
from annet.rpl import (
|
|
6
|
+
CommunityActionValue,
|
|
7
|
+
ResultType, RoutingPolicyStatement, RoutingPolicy, ConditionOperator, SingleCondition, SingleAction, ActionType,
|
|
8
|
+
)
|
|
9
|
+
from annet.rpl.statement_builder import AsPathActionValue, NextHopActionValue
|
|
10
|
+
from annet.storage import Storage
|
|
11
|
+
from .items import AS_PATH_FILTERS, COMMUNITIES, EXT_COMMUNITIES
|
|
12
|
+
from .route_policy import routemap
|
|
13
|
+
|
|
14
|
+
HUAWEI_MATCH_COMMAND_MAP = {
|
|
15
|
+
"as_path_filter": "as-path-filter {option_value}",
|
|
16
|
+
"metric": "cost {option_value}",
|
|
17
|
+
"protocol": "protocol {option_value}",
|
|
18
|
+
"interface": "interface {option_value}",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
HUAWEI_THEN_COMMAND_MAP = {
|
|
22
|
+
"metric": "cost {option_value}",
|
|
23
|
+
"local_pref": "local-preference {option_value}",
|
|
24
|
+
"metric_type": "cost-type {option_value}",
|
|
25
|
+
"mpls_label": "mpls-label",
|
|
26
|
+
"origin": "origin {option_value}",
|
|
27
|
+
"tag": "tag {option_value}",
|
|
28
|
+
# unsupported: resolution
|
|
29
|
+
# unsupported: rpki_valid_state
|
|
30
|
+
}
|
|
31
|
+
HUAWEI_RESULT_MAP = {
|
|
32
|
+
ResultType.ALLOW: "permit",
|
|
33
|
+
ResultType.DENY: "deny",
|
|
34
|
+
ResultType.NEXT: ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RoutingPolicyGenerator(PartialGenerator):
|
|
39
|
+
TAGS = ["policy", "rpl", "routing"]
|
|
40
|
+
|
|
41
|
+
def acl_huawei(self, _):
|
|
42
|
+
return r"""
|
|
43
|
+
ip as-path-filter
|
|
44
|
+
route-policy *
|
|
45
|
+
~ %global=1
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def _huawei_match(self, device, condition: SingleCondition[Any]) -> Iterator[Sequence[str]]:
|
|
49
|
+
if condition.field == "community":
|
|
50
|
+
if condition.operator is ConditionOperator.HAS:
|
|
51
|
+
if len(condition.value) > 1:
|
|
52
|
+
raise NotImplementedError("Multiple HAS for communities is not supported for huawei")
|
|
53
|
+
elif condition.operator is not ConditionOperator.HAS_ANY:
|
|
54
|
+
raise NotImplementedError("Community operator %r not supported for huawei" % condition.operator)
|
|
55
|
+
for comm_name in condition.value:
|
|
56
|
+
yield "if-match community-filter", comm_name
|
|
57
|
+
return
|
|
58
|
+
if condition.field == "extcommunity":
|
|
59
|
+
if condition.operator is ConditionOperator.HAS:
|
|
60
|
+
if len(condition.value) > 1:
|
|
61
|
+
raise NotImplementedError("Multiple HAS for extcommunities is not supported for huawei")
|
|
62
|
+
elif condition.operator is not ConditionOperator.HAS_ANY:
|
|
63
|
+
raise NotImplementedError("Extcommunity operator %r not supported for huawei" % condition.operator)
|
|
64
|
+
for comm_name in condition.value:
|
|
65
|
+
yield "if-match extcommunity-filter", comm_name
|
|
66
|
+
return
|
|
67
|
+
if condition.field == "ip_prefix":
|
|
68
|
+
for name in condition.value.names:
|
|
69
|
+
yield "if-match", "ip-prefix-filter", name
|
|
70
|
+
return
|
|
71
|
+
if condition.field == "ipv6_prefix":
|
|
72
|
+
for name in condition.value.names:
|
|
73
|
+
yield "if-match", "ipv6 address prefix-list", name
|
|
74
|
+
return
|
|
75
|
+
if condition.field == "as_path_length":
|
|
76
|
+
if condition.operator is ConditionOperator.EQ:
|
|
77
|
+
yield "if-match", "as-path length", condition.value
|
|
78
|
+
elif condition.operator is ConditionOperator.LE:
|
|
79
|
+
yield "if-match", "as-path length less-equal", condition.value
|
|
80
|
+
elif condition.operator is ConditionOperator.GE:
|
|
81
|
+
yield "if-match", "as-path length greater-equal", condition.value
|
|
82
|
+
elif condition.operator is ConditionOperator.BETWEEN_INCLUDED:
|
|
83
|
+
yield "if-match", "as-path length greater-equal", condition.value[0], "less-equal", condition.value[1]
|
|
84
|
+
else:
|
|
85
|
+
raise NotImplementedError(
|
|
86
|
+
f"as_path_length operator {condition.operator} not supported for huawei",
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
if condition.operator is not ConditionOperator.EQ:
|
|
90
|
+
raise NotImplementedError(
|
|
91
|
+
f"`{condition.field}` with operator {condition.operator} is not supported for huawei",
|
|
92
|
+
)
|
|
93
|
+
if condition.field not in HUAWEI_MATCH_COMMAND_MAP:
|
|
94
|
+
raise NotImplementedError(f"Match using `{condition.field}` is not supported for huawei")
|
|
95
|
+
cmd = HUAWEI_MATCH_COMMAND_MAP[condition.field]
|
|
96
|
+
yield "if-match", cmd.format(option_value=condition.value)
|
|
97
|
+
|
|
98
|
+
def _huawei_then_community(self, action: SingleAction[CommunityActionValue]) -> Iterator[Sequence[str]]:
|
|
99
|
+
if action.value.replaced is not None:
|
|
100
|
+
if not action.value.replaced:
|
|
101
|
+
yield "apply", "community", "none"
|
|
102
|
+
first = True
|
|
103
|
+
for community_name in action.value.replaced:
|
|
104
|
+
community = COMMUNITIES[community_name]
|
|
105
|
+
for comm_value in community.values:
|
|
106
|
+
if first:
|
|
107
|
+
yield "apply", "community", comm_value
|
|
108
|
+
first = False
|
|
109
|
+
else:
|
|
110
|
+
yield "apply", "community", comm_value, "additive"
|
|
111
|
+
for community_name in action.value.added:
|
|
112
|
+
community = COMMUNITIES[community_name]
|
|
113
|
+
for comm_value in community.values:
|
|
114
|
+
yield "apply", "community", comm_value, "additive"
|
|
115
|
+
for community_name in action.value.removed:
|
|
116
|
+
yield "apply comm-filter", community_name, "delete"
|
|
117
|
+
|
|
118
|
+
def _huawei_then_extcommunity(self, action: SingleAction[CommunityActionValue]) -> Iterator[Sequence[str]]:
|
|
119
|
+
if action.value.replaced is not None:
|
|
120
|
+
if not action.value.replaced:
|
|
121
|
+
yield "apply", "extcommunity", "none"
|
|
122
|
+
first = True
|
|
123
|
+
for community_name in action.value.replaced:
|
|
124
|
+
community = EXT_COMMUNITIES[community_name]
|
|
125
|
+
for comm_value in community.values:
|
|
126
|
+
if first:
|
|
127
|
+
yield "apply", "extcommunity", comm_value
|
|
128
|
+
first = False
|
|
129
|
+
else:
|
|
130
|
+
yield "apply", "extcommunity", comm_value, "additive"
|
|
131
|
+
for community_name in action.value.added:
|
|
132
|
+
community = EXT_COMMUNITIES[community_name]
|
|
133
|
+
for comm_value in community.values:
|
|
134
|
+
yield "apply", "extcommunity", comm_value, "additive"
|
|
135
|
+
for community_name in action.value.removed:
|
|
136
|
+
yield "apply extcommunity-filter", community_name, "delete"
|
|
137
|
+
|
|
138
|
+
def _huawei_then(self, device, action: SingleAction[Any]) -> Iterator[Sequence[str]]:
|
|
139
|
+
if action.field == "community":
|
|
140
|
+
yield from self._huawei_then_community(cast(SingleAction[CommunityActionValue], action))
|
|
141
|
+
return
|
|
142
|
+
if action.field == "extcommunity":
|
|
143
|
+
yield from self._huawei_then_extcommunity(cast(SingleAction[CommunityActionValue], action))
|
|
144
|
+
return
|
|
145
|
+
if action.field == "metric":
|
|
146
|
+
if action.type is ActionType.ADD:
|
|
147
|
+
yield "apply", f"cost + {action.value}"
|
|
148
|
+
elif action.type is ActionType.SET:
|
|
149
|
+
yield "apply", f"cost {action.value}"
|
|
150
|
+
else:
|
|
151
|
+
raise NotImplementedError(f"Action type {action.type} for metric is not supported for huawei")
|
|
152
|
+
return
|
|
153
|
+
if action.field == "as_path":
|
|
154
|
+
as_path_action_value = cast(AsPathActionValue, action.value)
|
|
155
|
+
if as_path_action_value.set is not None:
|
|
156
|
+
if not as_path_action_value.set:
|
|
157
|
+
yield "apply", "as_path", "none overwrite"
|
|
158
|
+
first = True
|
|
159
|
+
for path_item in as_path_action_value.set:
|
|
160
|
+
if first:
|
|
161
|
+
yield "apply as-path", path_item, "overwrite"
|
|
162
|
+
first = False
|
|
163
|
+
else:
|
|
164
|
+
yield "apply as-path", path_item, "additive"
|
|
165
|
+
if as_path_action_value.prepend:
|
|
166
|
+
for path_item in as_path_action_value.prepend:
|
|
167
|
+
yield "apply as-path", path_item, "additive"
|
|
168
|
+
if as_path_action_value.expand: # same as prepend?
|
|
169
|
+
for path_item in as_path_action_value.expand:
|
|
170
|
+
yield "apply as-path", path_item, "additive"
|
|
171
|
+
if as_path_action_value.delete:
|
|
172
|
+
for path_item in as_path_action_value.delete:
|
|
173
|
+
yield "apply as-path", path_item, "delete"
|
|
174
|
+
if as_path_action_value.expand_last_as:
|
|
175
|
+
raise RuntimeError("asp_path.expand_last_as is not supported for huawei")
|
|
176
|
+
return
|
|
177
|
+
if action.field == "next_hop":
|
|
178
|
+
next_hop_action_value = cast(NextHopActionValue, action.value)
|
|
179
|
+
if next_hop_action_value.target == "self":
|
|
180
|
+
yield "apply", "cost 1"
|
|
181
|
+
elif next_hop_action_value.target == "discard":
|
|
182
|
+
pass
|
|
183
|
+
elif next_hop_action_value.target == "peer":
|
|
184
|
+
pass
|
|
185
|
+
elif next_hop_action_value.target == "ipv4_addr":
|
|
186
|
+
yield "apply", f"ip-address next-hop {next_hop_action_value.addr}"
|
|
187
|
+
elif next_hop_action_value.target == "ipv6_addr":
|
|
188
|
+
yield "apply", f"ipv6 next-hop {next_hop_action_value.addr}"
|
|
189
|
+
elif next_hop_action_value.target == "mapped_ipv4":
|
|
190
|
+
yield "apply", f"ipv6 next-hop ::FFFF:{next_hop_action_value.addr}"
|
|
191
|
+
else:
|
|
192
|
+
raise RuntimeError(f"Next_hop target {next_hop_action_value.target} is not supported for huawei")
|
|
193
|
+
|
|
194
|
+
if action.type is not ActionType.SET:
|
|
195
|
+
raise NotImplementedError(f"Action type {action.type} for `{action.field}` is not supported for huawei")
|
|
196
|
+
if action.field not in HUAWEI_THEN_COMMAND_MAP:
|
|
197
|
+
raise NotImplementedError(f"Then action using `{action.field}` is not supported for huawei")
|
|
198
|
+
cmd = HUAWEI_THEN_COMMAND_MAP[action.field]
|
|
199
|
+
yield "apply", cmd.format(option_value=action.value)
|
|
200
|
+
|
|
201
|
+
def _huawei_statement(
|
|
202
|
+
self, device, policy: RoutingPolicy, statement: RoutingPolicyStatement,
|
|
203
|
+
) -> Iterator[Sequence[str]]:
|
|
204
|
+
if "as_path_filter" in statement.match:
|
|
205
|
+
as_path_condition = statement.match["as_path_filter"]
|
|
206
|
+
as_filter_value = AS_PATH_FILTERS[as_path_condition.value]
|
|
207
|
+
yield "ip as-path-filter", \
|
|
208
|
+
as_path_condition.value, \
|
|
209
|
+
"index 10 permit", \
|
|
210
|
+
"_{}_".format("_".join(("%s" % x for x in as_filter_value if x != ".*")))
|
|
211
|
+
|
|
212
|
+
with self.block(
|
|
213
|
+
"route-policy", policy.name,
|
|
214
|
+
HUAWEI_RESULT_MAP[statement.result],
|
|
215
|
+
"node", statement.number
|
|
216
|
+
):
|
|
217
|
+
for condition in statement.match:
|
|
218
|
+
yield from self._huawei_match(device, condition)
|
|
219
|
+
for action in statement.then:
|
|
220
|
+
yield from self._huawei_then(device, action)
|
|
221
|
+
if statement.result is ResultType.NEXT:
|
|
222
|
+
yield "goto next-node"
|
|
223
|
+
|
|
224
|
+
def run_huawei(self, device):
|
|
225
|
+
for policy in routemap.apply(device):
|
|
226
|
+
for statement in policy.statements:
|
|
227
|
+
yield from self._huawei_statement(device, policy, statement)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_generators(store: Storage) -> list[BaseGenerator]:
|
|
231
|
+
return [
|
|
232
|
+
RoutingPolicyGenerator(store),
|
|
233
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from annet.adapters.netbox.common.models import NetboxDevice
|
|
2
|
+
from annet.rpl import R, RouteMap, Route
|
|
3
|
+
|
|
4
|
+
routemap = RouteMap[NetboxDevice]()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@routemap
|
|
8
|
+
def example1(device: NetboxDevice, route: Route):
|
|
9
|
+
condition = (R.interface == "l0.0") & (R.protocol == "bgp")
|
|
10
|
+
with route(condition, number=1, name="n1") as rule:
|
|
11
|
+
rule.set_local_pref(100)
|
|
12
|
+
rule.set_metric(100)
|
|
13
|
+
rule.add_metric(200)
|
|
14
|
+
rule.community.set("COMMUNITY_EXAMPLE_ADD")
|
|
15
|
+
rule.as_path.set(12345, "123456")
|
|
16
|
+
rule.allow()
|
|
17
|
+
with route(R.protocol == "bgp", R.community.has("comm_name"), number=2, name="n2") as rule:
|
|
18
|
+
rule.set_local_pref(100)
|
|
19
|
+
rule.add_metric(200)
|
|
20
|
+
rule.community.add("COMMUNITY_EXAMPLE_ADD")
|
|
21
|
+
rule.community.remove("COMMUNITY_EXAMPLE_REMOVE")
|
|
22
|
+
rule.allow()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@routemap
|
|
26
|
+
def example2(device: NetboxDevice, route: Route):
|
|
27
|
+
with route(R.as_path_filter("ASP_EXAMPLE"), number=3, name="n3") as rule:
|
|
28
|
+
rule.deny()
|
|
29
|
+
with route(R.match_v6("IPV6_LIST_EXAMPLE"), number=4, name="n4") as rule:
|
|
30
|
+
rule.allow()
|
|
31
|
+
|
|
32
|
+
with route(R.as_path_length >= 1, R.as_path_length <= 20, number=4, name="n4") as rule:
|
|
33
|
+
rule.allow()
|
annet-0.16.16.dist-info/METADATA
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: annet
|
|
3
|
-
Version: 0.16.16
|
|
4
|
-
Summary: annet
|
|
5
|
-
Home-page: https://github.com/annetutil/annet
|
|
6
|
-
License: MIT
|
|
7
|
-
Requires-Python: >=3.10
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
License-File: AUTHORS
|
|
10
|
-
Requires-Dist: colorama >=0.4.6
|
|
11
|
-
Requires-Dist: tabulate >=0.9.0
|
|
12
|
-
Requires-Dist: jsonpatch >=1.33
|
|
13
|
-
Requires-Dist: jsonpointer >=2.4
|
|
14
|
-
Requires-Dist: PyYAML >=6.0.1
|
|
15
|
-
Requires-Dist: Pygments >=2.14.0
|
|
16
|
-
Requires-Dist: Mako >=1.2.4
|
|
17
|
-
Requires-Dist: Jinja2 >=3.1.2
|
|
18
|
-
Requires-Dist: psutil >=5.8.0
|
|
19
|
-
Requires-Dist: packaging >=23.2
|
|
20
|
-
Requires-Dist: contextlog >=1.1
|
|
21
|
-
Requires-Dist: valkit >=0.1.4
|
|
22
|
-
Requires-Dist: yarl >=1.8.2
|
|
23
|
-
Requires-Dist: adaptix ==3.0.0b7
|
|
24
|
-
Requires-Dist: dataclass-rest ==0.4
|
|
25
|
-
Provides-Extra: netbox
|
|
26
|
-
Requires-Dist: annetbox[sync] >=0.1.8 ; extra == 'netbox'
|
|
27
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|