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.
Files changed (29) hide show
  1. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/PKG-INFO +1 -1
  2. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/pyproject.toml +1 -1
  3. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/__init__.py +2 -0
  4. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/drivers/base.py +29 -5
  5. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/composed_configuration.py +6 -0
  6. acex_devkit-1.8.0/src/acex_devkit/normalizer/__init__.py +22 -0
  7. acex_devkit-1.8.0/src/acex_devkit/normalizer/base.py +80 -0
  8. acex_devkit-1.8.0/src/acex_devkit/normalizer/engine.py +162 -0
  9. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/README.md +0 -0
  10. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/__init__.py +0 -0
  11. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/command.py +0 -0
  12. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/configdiffer.py +0 -0
  13. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/diff.py +0 -0
  14. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/old_configdiffer.py +0 -0
  15. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/configdiffer/old_diff.py +0 -0
  16. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/drivers/__init__.py +0 -0
  17. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/drivers/base_driver.py +0 -0
  18. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/exceptions/__init__.py +0 -0
  19. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/__init__.py +0 -0
  20. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/acl_model.py +0 -0
  21. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/attribute_value.py +0 -0
  22. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/container_entry.py +0 -0
  23. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/external_value.py +0 -0
  24. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/logging.py +0 -0
  25. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/management_connection.py +0 -0
  26. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/ned.py +0 -0
  27. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/node_response.py +0 -0
  28. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/models/spanning_tree.py +0 -0
  29. {acex_devkit-1.7.0 → acex_devkit-1.8.0}/src/acex_devkit/types/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acex-devkit
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: ACE-X DevKit - Development kit for building ACE-X drivers and plugins
5
5
  License: AGPL-3.0
6
6
  Keywords: automation,devkit,sdk,drivers,plugins
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "acex-devkit"
3
- version = "1.7.0"
3
+ version = "1.8.0"
4
4
  description = "ACE-X DevKit - Development kit for building ACE-X drivers and plugins"
5
5
  authors = ["Johan Lahti <johan.lahti@acebit.se>"]
6
6
  readme = "README.md"
@@ -8,10 +8,12 @@ from acex_devkit.drivers import (
8
8
  RendererBase,
9
9
  ParserBase,
10
10
  )
11
+ from acex_devkit.normalizer import BaseNormalizer
11
12
 
12
13
  __all__ = [
13
14
  "NetworkElementDriver",
14
15
  "TransportBase",
15
16
  "RendererBase",
16
17
  "ParserBase",
18
+ "BaseNormalizer",
17
19
  ]
@@ -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