acex-devkit 1.7.0__tar.gz → 1.8.0__tar.gz
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.
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/PKG-INFO +1 -1
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/pyproject.toml +1 -1
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/__init__.py +2 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/drivers/base.py +29 -5
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/composed_configuration.py +6 -0
- acex_devkit-1.8.0/src/acex_devkit/normalizer/__init__.py +22 -0
- acex_devkit-1.8.0/src/acex_devkit/normalizer/base.py +80 -0
- acex_devkit-1.8.0/src/acex_devkit/normalizer/engine.py +162 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/README.md +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/__init__.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/command.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/configdiffer.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/diff.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/old_configdiffer.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/old_diff.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/drivers/__init__.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/drivers/base_driver.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/exceptions/__init__.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/__init__.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/acl_model.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/attribute_value.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/container_entry.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/external_value.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/logging.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/management_connection.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/ned.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/node_response.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/spanning_tree.py +0 -0
- {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/types/__init__.py +0 -0
|
@@ -70,19 +70,22 @@ class TransportBase(ABC):
|
|
|
70
70
|
|
|
71
71
|
class NetworkElementDriver:
|
|
72
72
|
"""Base class for network element drivers.
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
Combines renderer, transport, and parser to provide complete
|
|
75
75
|
configuration management for network devices.
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
Attributes:
|
|
78
78
|
renderer_class: Renderer class to use (must be set in subclass)
|
|
79
79
|
transport_class: Transport class to use (must be set in subclass)
|
|
80
80
|
parser_class: Parser class to use (must be set in subclass)
|
|
81
|
+
normalizer_class: Optional normalizer class for stripping
|
|
82
|
+
non-intent config and masking secrets
|
|
81
83
|
"""
|
|
82
|
-
|
|
84
|
+
|
|
83
85
|
renderer_class = None
|
|
84
86
|
transport_class = None
|
|
85
87
|
parser_class = None
|
|
88
|
+
normalizer_class = None
|
|
86
89
|
|
|
87
90
|
def __init__(self):
|
|
88
91
|
"""Initialize driver with renderer, transport, and parser instances."""
|
|
@@ -93,6 +96,7 @@ class NetworkElementDriver:
|
|
|
93
96
|
self.renderer = self.renderer_class()
|
|
94
97
|
self.transport = self.transport_class()
|
|
95
98
|
self.parser = self.parser_class()
|
|
99
|
+
self.normalizer = self.normalizer_class() if self.normalizer_class else None
|
|
96
100
|
|
|
97
101
|
@abstractmethod
|
|
98
102
|
def render(self, logical_node: "LogicalNode", asset: Any = None) -> Any:
|
|
@@ -110,11 +114,31 @@ class NetworkElementDriver:
|
|
|
110
114
|
@abstractmethod
|
|
111
115
|
def parse(self, configuration: str) -> Any:
|
|
112
116
|
"""Parse device configuration.
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
Args:
|
|
115
119
|
configuration: Raw device configuration
|
|
116
|
-
|
|
120
|
+
|
|
117
121
|
Returns:
|
|
118
122
|
Parsed configuration model
|
|
119
123
|
"""
|
|
120
124
|
return self.parser.parse(configuration)
|
|
125
|
+
|
|
126
|
+
def normalize(self, raw: str) -> str:
|
|
127
|
+
"""Strip non-intent data (timestamps, auto-generated certs, etc.).
|
|
128
|
+
|
|
129
|
+
Returns the cleaned config string. If no normalizer is configured
|
|
130
|
+
the input is returned unchanged.
|
|
131
|
+
"""
|
|
132
|
+
if self.normalizer is None:
|
|
133
|
+
return raw
|
|
134
|
+
return self.normalizer.normalize(raw).config
|
|
135
|
+
|
|
136
|
+
def mask(self, raw: str) -> str:
|
|
137
|
+
"""Replace secrets with <REDACTED>.
|
|
138
|
+
|
|
139
|
+
Returns the masked config string. If no normalizer is configured
|
|
140
|
+
the input is returned unchanged.
|
|
141
|
+
"""
|
|
142
|
+
if self.normalizer is None:
|
|
143
|
+
return raw
|
|
144
|
+
return self.normalizer.mask(raw).config
|
|
@@ -704,6 +704,11 @@ class Dhcp(BaseModel):
|
|
|
704
704
|
snooping: Optional[DHCPSnoopingAttributes] = DHCPSnoopingAttributes()
|
|
705
705
|
relay: Optional[DhcpRelay] = DhcpRelay()
|
|
706
706
|
|
|
707
|
+
class Services(BaseModel):
|
|
708
|
+
name: Optional[AttributeValue[str]] = None
|
|
709
|
+
http: Optional[AttributeValue[bool]] = None # for webgui access
|
|
710
|
+
https: Optional[AttributeValue[bool]] = None # for webgui access
|
|
711
|
+
|
|
707
712
|
class System(BaseModel):
|
|
708
713
|
config: SystemConfig = SystemConfig()
|
|
709
714
|
aaa: Optional[TripleA] = TripleA()
|
|
@@ -713,6 +718,7 @@ class System(BaseModel):
|
|
|
713
718
|
snmp: Optional[Snmp] = Snmp()
|
|
714
719
|
vtp: Optional[VTP] = VTP()
|
|
715
720
|
dhcp: Optional[Dhcp] = Dhcp()
|
|
721
|
+
services: Optional[Services] = Services()
|
|
716
722
|
|
|
717
723
|
# For different types of interfaces that are fine for response model:
|
|
718
724
|
InterfaceType = Union[
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Configuration normalizer — vendor-agnostic engine for stripping
|
|
2
|
+
runtime noise and masking secrets from device configurations."""
|
|
3
|
+
|
|
4
|
+
from acex_devkit.normalizer.engine import (
|
|
5
|
+
LineRule,
|
|
6
|
+
BlockRule,
|
|
7
|
+
RewriteRule,
|
|
8
|
+
OpStats,
|
|
9
|
+
OpResult,
|
|
10
|
+
NormalizerEngine,
|
|
11
|
+
)
|
|
12
|
+
from acex_devkit.normalizer.base import BaseNormalizer
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"LineRule",
|
|
16
|
+
"BlockRule",
|
|
17
|
+
"RewriteRule",
|
|
18
|
+
"OpStats",
|
|
19
|
+
"OpResult",
|
|
20
|
+
"NormalizerEngine",
|
|
21
|
+
"BaseNormalizer",
|
|
22
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Base normalizer for vendor-specific subclasses.
|
|
2
|
+
|
|
3
|
+
Subclasses only need to declare rules:
|
|
4
|
+
|
|
5
|
+
class MyVendorNormalizer(BaseNormalizer):
|
|
6
|
+
line_rules = [...]
|
|
7
|
+
block_rules = [...]
|
|
8
|
+
rewrite_rules = [...]
|
|
9
|
+
|
|
10
|
+
Optional overrides if the vendor syntax differs:
|
|
11
|
+
is_block_continuation(line) -> bool
|
|
12
|
+
is_block_terminator(line) -> bool
|
|
13
|
+
post_process(lines, stats) -> list[str]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from acex_devkit.normalizer.engine import (
|
|
21
|
+
LineRule,
|
|
22
|
+
BlockRule,
|
|
23
|
+
RewriteRule,
|
|
24
|
+
OpStats,
|
|
25
|
+
OpResult,
|
|
26
|
+
NormalizerEngine,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseNormalizer:
|
|
31
|
+
line_rules: list[LineRule] = []
|
|
32
|
+
block_rules: list[BlockRule] = []
|
|
33
|
+
rewrite_rules: list[RewriteRule] = []
|
|
34
|
+
|
|
35
|
+
engine: NormalizerEngine = NormalizerEngine()
|
|
36
|
+
|
|
37
|
+
# ── hooks (override per vendor) ───────────────────────────────
|
|
38
|
+
|
|
39
|
+
def is_block_continuation(self, line: str) -> bool:
|
|
40
|
+
if not line:
|
|
41
|
+
return False
|
|
42
|
+
if line.startswith((" ", "\t")):
|
|
43
|
+
return True
|
|
44
|
+
if re.fullmatch(r"[0-9A-Fa-f ]+", line.rstrip()):
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def is_block_terminator(self, line: str) -> bool:
|
|
49
|
+
return line.strip() in ("quit", "exit")
|
|
50
|
+
|
|
51
|
+
def post_process(self, lines: list[str], stats: OpStats) -> list[str]:
|
|
52
|
+
return self.engine.collapse_and_trim(lines, stats)
|
|
53
|
+
|
|
54
|
+
# ── operations ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def normalize(self, raw: str) -> OpResult:
|
|
57
|
+
lines, stats = self.engine.normalize_pass(
|
|
58
|
+
raw,
|
|
59
|
+
line_rules=self.line_rules,
|
|
60
|
+
block_rules=self.block_rules,
|
|
61
|
+
is_block_continuation=self.is_block_continuation,
|
|
62
|
+
is_block_terminator=self.is_block_terminator,
|
|
63
|
+
)
|
|
64
|
+
lines = self.post_process(lines, stats)
|
|
65
|
+
stats.lines_out = len(lines)
|
|
66
|
+
return OpResult(
|
|
67
|
+
config="\n".join(lines) + ("\n" if lines else ""),
|
|
68
|
+
operation="normalize",
|
|
69
|
+
stats=stats,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def mask(self, raw: str) -> OpResult:
|
|
73
|
+
lines, stats = self.engine.mask_pass(
|
|
74
|
+
raw, rewrite_rules=self.rewrite_rules
|
|
75
|
+
)
|
|
76
|
+
return OpResult(
|
|
77
|
+
config="\n".join(lines) + ("\n" if lines else ""),
|
|
78
|
+
operation="mask",
|
|
79
|
+
stats=stats,
|
|
80
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Stateless rule engine and data types for config normalization.
|
|
2
|
+
|
|
3
|
+
Two independent passes:
|
|
4
|
+
normalize_pass — drops lines/blocks that are not intent-config
|
|
5
|
+
mask_pass — rewrites lines to redact secrets
|
|
6
|
+
|
|
7
|
+
The passes are isolated: normalize never touches content it keeps,
|
|
8
|
+
mask never drops lines. This makes the operations order-independent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Rule types ────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class LineRule:
|
|
21
|
+
"""Matches a full line → the line is dropped."""
|
|
22
|
+
name: str
|
|
23
|
+
pattern: re.Pattern
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class BlockRule:
|
|
28
|
+
"""Matches a block header → header + indented body are dropped."""
|
|
29
|
+
name: str
|
|
30
|
+
pattern: re.Pattern
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RewriteRule:
|
|
35
|
+
"""Rewrites a line (e.g. redact secrets)."""
|
|
36
|
+
name: str
|
|
37
|
+
pattern: re.Pattern
|
|
38
|
+
replacement: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Result types ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class OpStats:
|
|
45
|
+
lines_in: int = 0
|
|
46
|
+
lines_out: int = 0
|
|
47
|
+
lines_dropped: int = 0
|
|
48
|
+
blocks_dropped: int = 0
|
|
49
|
+
lines_redacted: int = 0
|
|
50
|
+
by_rule: dict[str, int] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def _bump(self, rule: str) -> None:
|
|
53
|
+
self.by_rule[rule] = self.by_rule.get(rule, 0) + 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class OpResult:
|
|
58
|
+
config: str
|
|
59
|
+
operation: str # "normalize" or "mask"
|
|
60
|
+
stats: OpStats
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── Engine ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
class NormalizerEngine:
|
|
66
|
+
"""Stateless rule engine used by BaseNormalizer subclasses."""
|
|
67
|
+
|
|
68
|
+
def normalize_pass(
|
|
69
|
+
self,
|
|
70
|
+
raw: str,
|
|
71
|
+
*,
|
|
72
|
+
line_rules: list[LineRule],
|
|
73
|
+
block_rules: list[BlockRule],
|
|
74
|
+
is_block_continuation,
|
|
75
|
+
is_block_terminator,
|
|
76
|
+
) -> tuple[list[str], OpStats]:
|
|
77
|
+
stats = OpStats()
|
|
78
|
+
lines = raw.splitlines()
|
|
79
|
+
stats.lines_in = len(lines)
|
|
80
|
+
out: list[str] = []
|
|
81
|
+
|
|
82
|
+
i = 0
|
|
83
|
+
while i < len(lines):
|
|
84
|
+
line = lines[i]
|
|
85
|
+
|
|
86
|
+
block_hit = next(
|
|
87
|
+
(r for r in block_rules if r.pattern.match(line)), None
|
|
88
|
+
)
|
|
89
|
+
if block_hit:
|
|
90
|
+
stats.blocks_dropped += 1
|
|
91
|
+
stats._bump(f"block:{block_hit.name}")
|
|
92
|
+
stats.lines_dropped += 1
|
|
93
|
+
i += 1
|
|
94
|
+
while i < len(lines) and (
|
|
95
|
+
is_block_continuation(lines[i])
|
|
96
|
+
or is_block_terminator(lines[i])
|
|
97
|
+
):
|
|
98
|
+
stats.lines_dropped += 1
|
|
99
|
+
i += 1
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
line_hit = next(
|
|
103
|
+
(r for r in line_rules if r.pattern.match(line)), None
|
|
104
|
+
)
|
|
105
|
+
if line_hit:
|
|
106
|
+
stats.lines_dropped += 1
|
|
107
|
+
stats._bump(f"line:{line_hit.name}")
|
|
108
|
+
i += 1
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
out.append(line)
|
|
112
|
+
i += 1
|
|
113
|
+
|
|
114
|
+
return out, stats
|
|
115
|
+
|
|
116
|
+
def mask_pass(
|
|
117
|
+
self,
|
|
118
|
+
raw: str,
|
|
119
|
+
*,
|
|
120
|
+
rewrite_rules: list[RewriteRule],
|
|
121
|
+
) -> tuple[list[str], OpStats]:
|
|
122
|
+
stats = OpStats()
|
|
123
|
+
lines = raw.splitlines()
|
|
124
|
+
stats.lines_in = len(lines)
|
|
125
|
+
out: list[str] = []
|
|
126
|
+
|
|
127
|
+
for line in lines:
|
|
128
|
+
rewritten = line
|
|
129
|
+
for rule in rewrite_rules:
|
|
130
|
+
new = rule.pattern.sub(rule.replacement, rewritten)
|
|
131
|
+
if new != rewritten:
|
|
132
|
+
stats.lines_redacted += 1
|
|
133
|
+
stats._bump(f"redact:{rule.name}")
|
|
134
|
+
rewritten = new
|
|
135
|
+
break
|
|
136
|
+
out.append(rewritten)
|
|
137
|
+
|
|
138
|
+
stats.lines_out = len(out)
|
|
139
|
+
return out, stats
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def collapse_and_trim(
|
|
143
|
+
lines: list[str],
|
|
144
|
+
stats: OpStats,
|
|
145
|
+
comment_marker: str = "!",
|
|
146
|
+
) -> list[str]:
|
|
147
|
+
collapsed: list[str] = []
|
|
148
|
+
prev_marker = False
|
|
149
|
+
for line in lines:
|
|
150
|
+
is_marker = line.strip() == comment_marker
|
|
151
|
+
if is_marker and prev_marker:
|
|
152
|
+
stats.lines_dropped += 1
|
|
153
|
+
stats._bump("post:collapse_comment")
|
|
154
|
+
continue
|
|
155
|
+
collapsed.append(line)
|
|
156
|
+
prev_marker = is_marker
|
|
157
|
+
|
|
158
|
+
while collapsed and collapsed[0].strip() in ("", comment_marker):
|
|
159
|
+
collapsed.pop(0)
|
|
160
|
+
while collapsed and collapsed[-1].strip() in ("", comment_marker):
|
|
161
|
+
collapsed.pop()
|
|
162
|
+
return collapsed
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|