acex-devkit 1.1.2__tar.gz → 1.2.1__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.1.2 → acex_devkit-1.2.1}/PKG-INFO +1 -1
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/pyproject.toml +1 -1
- acex_devkit-1.2.1/src/acex_devkit/configdiffer/configdiffer.py +158 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/configdiffer/diff.py +4 -2
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/acl_model.py +4 -4
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/composed_configuration.py +81 -76
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/logging.py +21 -21
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/spanning_tree.py +17 -17
- acex_devkit-1.1.2/src/acex_devkit/configdiffer/configdiffer.py +0 -178
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/README.md +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/__init__.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/configdiffer/__init__.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/configdiffer/command.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/configdiffer/old_configdiffer.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/configdiffer/old_diff.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/drivers/__init__.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/drivers/base.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/drivers/base_driver.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/exceptions/__init__.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/__init__.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/attribute_value.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/external_value.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/ned.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/models/node_response.py +0 -0
- {acex_devkit-1.1.2 → acex_devkit-1.2.1}/src/acex_devkit/types/__init__.py +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Any, Dict, List, Optional, get_origin, get_args
|
|
3
|
+
|
|
4
|
+
from acex_devkit.models.composed_configuration import ComposedConfiguration
|
|
5
|
+
from acex_devkit.models.attribute_value import AttributeValue
|
|
6
|
+
from acex_devkit.configdiffer.diff import Diff, ComponentChange, AttributeChange, ComponentDiffOp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigDiffer:
|
|
10
|
+
|
|
11
|
+
def _is_dict_of_models(self, annotation) -> bool:
|
|
12
|
+
"""Check if a type annotation is Dict[str, SomeBaseModel]."""
|
|
13
|
+
origin = get_origin(annotation)
|
|
14
|
+
if origin is dict:
|
|
15
|
+
args = get_args(annotation)
|
|
16
|
+
if len(args) == 2 and isinstance(args[1], type) and issubclass(args[1], BaseModel):
|
|
17
|
+
return True
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
def _is_base_model(self, annotation) -> bool:
|
|
21
|
+
"""Check if a type annotation is a BaseModel subclass (not Dict, not AttributeValue)."""
|
|
22
|
+
return (
|
|
23
|
+
isinstance(annotation, type)
|
|
24
|
+
and issubclass(annotation, BaseModel)
|
|
25
|
+
and not issubclass(annotation, AttributeValue)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def _unwrap_optional(self, annotation):
|
|
29
|
+
"""Unwrap Optional[X] to X. Returns the annotation unchanged if not Optional."""
|
|
30
|
+
origin = get_origin(annotation)
|
|
31
|
+
if origin is type(None):
|
|
32
|
+
return annotation
|
|
33
|
+
args = get_args(annotation)
|
|
34
|
+
if args:
|
|
35
|
+
# Optional[X] is Union[X, None]
|
|
36
|
+
non_none = [a for a in args if a is not type(None)]
|
|
37
|
+
if len(non_none) == 1:
|
|
38
|
+
return non_none[0]
|
|
39
|
+
return annotation
|
|
40
|
+
|
|
41
|
+
def _flatten(self, model: BaseModel, path: tuple = ()) -> Dict[tuple, BaseModel]:
|
|
42
|
+
"""
|
|
43
|
+
Recursively walk a Pydantic model using type annotations to find
|
|
44
|
+
all components. Returns a flat mapping of path → model instance.
|
|
45
|
+
|
|
46
|
+
Rules:
|
|
47
|
+
- Dict[str, BaseModel] → component collection, each entry is a leaf
|
|
48
|
+
- BaseModel (not in Dict) → container, recurse deeper
|
|
49
|
+
- AttributeValue / primitives / None → skip (attributes, not components)
|
|
50
|
+
"""
|
|
51
|
+
result = {}
|
|
52
|
+
|
|
53
|
+
for field_name, field_info in model.model_fields.items():
|
|
54
|
+
annotation = self._unwrap_optional(field_info.annotation)
|
|
55
|
+
value = getattr(model, field_name)
|
|
56
|
+
|
|
57
|
+
if value is None:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
if self._is_dict_of_models(annotation):
|
|
61
|
+
# Component collection: each entry is a diffable component
|
|
62
|
+
for key, component in value.items():
|
|
63
|
+
component_path = path + (field_name, key)
|
|
64
|
+
result[component_path] = component
|
|
65
|
+
|
|
66
|
+
elif self._is_base_model(annotation):
|
|
67
|
+
# Recurse deeper — if nothing found, this model itself is a leaf component
|
|
68
|
+
child_path = path + (field_name,)
|
|
69
|
+
deeper = self._flatten(value, child_path)
|
|
70
|
+
if deeper:
|
|
71
|
+
result.update(deeper)
|
|
72
|
+
else:
|
|
73
|
+
result[child_path] = value
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def _attribute_changes(self, before: BaseModel, after: BaseModel) -> List[AttributeChange]:
|
|
78
|
+
"""
|
|
79
|
+
Compare two component model instances and return a list of changed attributes.
|
|
80
|
+
"""
|
|
81
|
+
changes = []
|
|
82
|
+
all_fields = set(before.model_fields.keys()) | set(after.model_fields.keys())
|
|
83
|
+
|
|
84
|
+
for field_name in all_fields:
|
|
85
|
+
b = getattr(before, field_name, None)
|
|
86
|
+
a = getattr(after, field_name, None)
|
|
87
|
+
if b != a:
|
|
88
|
+
changes.append(AttributeChange(attribute_name=field_name, before=b, after=a))
|
|
89
|
+
|
|
90
|
+
return changes
|
|
91
|
+
|
|
92
|
+
def diff(self, *, desired_config: ComposedConfiguration, observed_config: ComposedConfiguration) -> Diff:
|
|
93
|
+
"""
|
|
94
|
+
Compare two ComposedConfiguration objects and return a component-based diff.
|
|
95
|
+
|
|
96
|
+
Uses Pydantic model introspection to find all components (entries in
|
|
97
|
+
Dict[str, BaseModel] fields). Components are identified by their full
|
|
98
|
+
path, e.g. ('interfaces', 'GigabitEthernet0/0/1').
|
|
99
|
+
"""
|
|
100
|
+
flat_desired = self._flatten(desired_config)
|
|
101
|
+
flat_observed = self._flatten(observed_config)
|
|
102
|
+
|
|
103
|
+
desired_paths = set(flat_desired.keys())
|
|
104
|
+
observed_paths = set(flat_observed.keys())
|
|
105
|
+
|
|
106
|
+
added = []
|
|
107
|
+
removed = []
|
|
108
|
+
changed = []
|
|
109
|
+
|
|
110
|
+
for path in desired_paths - observed_paths:
|
|
111
|
+
obj = flat_desired[path]
|
|
112
|
+
added.append(ComponentChange(
|
|
113
|
+
op=ComponentDiffOp.ADD,
|
|
114
|
+
path=list(path),
|
|
115
|
+
component_type=type(obj),
|
|
116
|
+
component_name=path[-1],
|
|
117
|
+
before=None,
|
|
118
|
+
after=obj,
|
|
119
|
+
before_dict=None,
|
|
120
|
+
after_dict=obj.model_dump(),
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
for path in observed_paths - desired_paths:
|
|
124
|
+
obj = flat_observed[path]
|
|
125
|
+
removed.append(ComponentChange(
|
|
126
|
+
op=ComponentDiffOp.REMOVE,
|
|
127
|
+
path=list(path),
|
|
128
|
+
component_type=type(obj),
|
|
129
|
+
component_name=path[-1],
|
|
130
|
+
before=obj,
|
|
131
|
+
after=None,
|
|
132
|
+
before_dict=obj.model_dump(),
|
|
133
|
+
after_dict=None,
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
for path in desired_paths & observed_paths:
|
|
137
|
+
desired_obj = flat_desired[path]
|
|
138
|
+
observed_obj = flat_observed[path]
|
|
139
|
+
if desired_obj != observed_obj:
|
|
140
|
+
changed.append(ComponentChange(
|
|
141
|
+
op=ComponentDiffOp.CHANGE,
|
|
142
|
+
path=list(path),
|
|
143
|
+
component_type=type(desired_obj),
|
|
144
|
+
component_name=path[-1],
|
|
145
|
+
before=observed_obj,
|
|
146
|
+
after=desired_obj,
|
|
147
|
+
before_dict=observed_obj.model_dump(),
|
|
148
|
+
after_dict=desired_obj.model_dump(),
|
|
149
|
+
changed_attributes=self._attribute_changes(observed_obj, desired_obj),
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
return Diff(
|
|
153
|
+
added=added,
|
|
154
|
+
removed=removed,
|
|
155
|
+
changed=changed,
|
|
156
|
+
total_desired=len(desired_paths),
|
|
157
|
+
total_observed=len(observed_paths),
|
|
158
|
+
)
|
|
@@ -22,9 +22,11 @@ class ComponentDiffOp(str, Enum):
|
|
|
22
22
|
|
|
23
23
|
class AttributeChange(BaseModel):
|
|
24
24
|
"""Represents a change to a single attribute within a component"""
|
|
25
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
26
|
+
|
|
25
27
|
attribute_name: str
|
|
26
|
-
before: Optional[
|
|
27
|
-
after: Optional[
|
|
28
|
+
before: Optional[Any] = None
|
|
29
|
+
after: Optional[Any] = None
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class ComponentChange(BaseModel):
|
|
@@ -67,13 +67,13 @@ class Ipv6AclEntryAttributes(IpAclOptions):
|
|
|
67
67
|
class Ipv4AclAttributes(BaseModel):
|
|
68
68
|
name: AttributeValue[str]
|
|
69
69
|
type: AttributeValue[str] = "ipv4_acl"
|
|
70
|
-
acl_entries: Optional[Dict[str, Ipv4AclEntryAttributes]] =
|
|
70
|
+
acl_entries: Optional[Dict[str, Ipv4AclEntryAttributes]] = None
|
|
71
71
|
|
|
72
72
|
class Ipv6AclAttributes(BaseModel):
|
|
73
73
|
name: AttributeValue[str]
|
|
74
74
|
type: AttributeValue[str] = "ipv6_acl"
|
|
75
|
-
acl_entries: Optional[Dict[str, Ipv6AclEntryAttributes]] =
|
|
75
|
+
acl_entries: Optional[Dict[str, Ipv6AclEntryAttributes]] = None
|
|
76
76
|
|
|
77
77
|
class Acl(BaseModel):
|
|
78
|
-
ipv4_acls: Optional[Dict[str, Ipv4AclAttributes]] =
|
|
79
|
-
ipv6_acls: Optional[Dict[str, Ipv6AclAttributes]] =
|
|
78
|
+
ipv4_acls: Optional[Dict[str, Ipv4AclAttributes]] = None
|
|
79
|
+
ipv6_acls: Optional[Dict[str, Ipv6AclAttributes]] = None
|
|
@@ -55,10 +55,10 @@ class SystemConfig(BaseModel):
|
|
|
55
55
|
class LoggingComponents(BaseModel):
|
|
56
56
|
config: LoggingConfig = LoggingConfig()
|
|
57
57
|
console: Optional[Console] = None
|
|
58
|
-
remote_servers: Optional[Dict[str, RemoteServer]] =
|
|
58
|
+
remote_servers: Optional[Dict[str, RemoteServer]] = None
|
|
59
59
|
events: Optional[LoggingEvents] = None
|
|
60
|
-
vty: Optional[Dict[str, VtyLine]] =
|
|
61
|
-
files: Optional[Dict[str, FileLogging]] =
|
|
60
|
+
vty: Optional[Dict[str, VtyLine]] = None
|
|
61
|
+
files: Optional[Dict[str, FileLogging]] = None
|
|
62
62
|
|
|
63
63
|
class NtpConfig(BaseModel):
|
|
64
64
|
enabled: AttributeValue[bool] = AttributeValue(value=False)
|
|
@@ -73,7 +73,7 @@ class NtpServer(BaseModel):
|
|
|
73
73
|
|
|
74
74
|
class Ntp(BaseModel):
|
|
75
75
|
config: Optional[NtpConfig] = None
|
|
76
|
-
servers: Optional[Dict[str, NtpServer]] =
|
|
76
|
+
servers: Optional[Dict[str, NtpServer]] = None
|
|
77
77
|
|
|
78
78
|
class SshServer(BaseModel):
|
|
79
79
|
enable: Optional[AttributeValue[bool]] = None
|
|
@@ -100,7 +100,7 @@ class AuthorizedKey(BaseModel):
|
|
|
100
100
|
|
|
101
101
|
class Ssh(BaseModel):
|
|
102
102
|
config: Optional[SshServer] = None
|
|
103
|
-
host_keys: Optional[Dict[str, AuthorizedKey]] =
|
|
103
|
+
host_keys: Optional[Dict[str, AuthorizedKey]] = None
|
|
104
104
|
|
|
105
105
|
class Lldp(BaseModel): ...
|
|
106
106
|
|
|
@@ -164,23 +164,22 @@ class EthernetCsmacdInterface(Interface):
|
|
|
164
164
|
lacp_interval: Optional[AttributeValue[Literal["fast", "slow"]]] = None
|
|
165
165
|
|
|
166
166
|
# Spanning-tree relaterade attribut
|
|
167
|
-
stp_port_priority: Optional[int] = None
|
|
168
|
-
stp_cost: Optional[int] = None
|
|
169
|
-
stp_edge_port: Optional[bool] =
|
|
170
|
-
stp_bpdu_filter: Optional[bool] =
|
|
171
|
-
stp_bpdu_guard: Optional[bool] =
|
|
172
|
-
stp_loop_guard: Optional[bool] =
|
|
173
|
-
stp_root_guard: Optional[bool] =
|
|
174
|
-
stp_portfast: Optional[bool] =
|
|
175
|
-
stp_link_type: Optional[Literal["point-to-point", "shared"]] = None
|
|
167
|
+
stp_port_priority: Optional[AttributeValue[int]] = None
|
|
168
|
+
stp_cost: Optional[AttributeValue[int]] = None
|
|
169
|
+
stp_edge_port: Optional[AttributeValue[bool]] = None
|
|
170
|
+
stp_bpdu_filter: Optional[AttributeValue[bool]] = None
|
|
171
|
+
stp_bpdu_guard: Optional[AttributeValue[bool]] = None
|
|
172
|
+
stp_loop_guard: Optional[AttributeValue[bool]] = None
|
|
173
|
+
stp_root_guard: Optional[AttributeValue[bool]] = None
|
|
174
|
+
stp_portfast: Optional[AttributeValue[bool]] = None
|
|
175
|
+
stp_link_type: Optional[AttributeValue[Literal["point-to-point", "shared"]]] = None
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
class Ieee8023adLagInterface(Interface):
|
|
179
179
|
"LAG Interface"
|
|
180
180
|
type: Literal["ieee8023adLag"] = "ieee8023adLag"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
members: list[str] = Field(default_factory=list)
|
|
181
|
+
aggregate_id: Optional[AttributeValue[int]] = None
|
|
182
|
+
members: Optional[AttributeValue[List[str]]] = None
|
|
184
183
|
max_ports: Optional[AttributeValue[int]] = None
|
|
185
184
|
switchport: Optional[AttributeValue[bool]] = None
|
|
186
185
|
switchport_mode: Optional[AttributeValue[Literal["access", "trunk"]]] = None
|
|
@@ -226,15 +225,15 @@ class StaticRouteNextHop(BaseModel):
|
|
|
226
225
|
class StaticRoute(BaseModel):
|
|
227
226
|
route_name: Optional[AttributeValue[str]] = None
|
|
228
227
|
prefix: AttributeValue[str]
|
|
229
|
-
next_hops: Optional[Dict[str, StaticRouteNextHop]] =
|
|
228
|
+
next_hops: Optional[Dict[str, StaticRouteNextHop]] = None
|
|
230
229
|
network_instance: Optional[AttributeValue[str]] = None
|
|
231
230
|
|
|
232
231
|
class Protocols(BaseModel):
|
|
233
|
-
static_routes: Optional[Dict[str, StaticRoute]] =
|
|
232
|
+
static_routes: Optional[Dict[str, StaticRoute]] = None
|
|
234
233
|
# OSPF, BGP, etc. can be added here as needed
|
|
235
234
|
|
|
236
235
|
class RouteTarget(BaseModel):
|
|
237
|
-
value: str
|
|
236
|
+
value: Optional[AttributeValue[str]] = None
|
|
238
237
|
|
|
239
238
|
class ImportExportPolicy(BaseModel):
|
|
240
239
|
export_route_target: Optional[List[RouteTarget]] = None
|
|
@@ -246,9 +245,9 @@ class InterInstancePolicy(BaseModel):
|
|
|
246
245
|
class NetworkInstance(BaseModel):
|
|
247
246
|
name: AttributeValue[str]
|
|
248
247
|
description: Optional[AttributeValue[str]] = None
|
|
249
|
-
vlans: Optional[Dict[str, Vlan]] =
|
|
248
|
+
vlans: Optional[Dict[str, Vlan]] = None
|
|
250
249
|
interfaces: Optional[Dict[str, Reference]] = {}
|
|
251
|
-
inter_instance_policies: Optional[Dict[str, InterInstancePolicy]] =
|
|
250
|
+
inter_instance_policies: Optional[Dict[str, InterInstancePolicy]] = None
|
|
252
251
|
protocols: Optional[Protocols] = Protocols()
|
|
253
252
|
|
|
254
253
|
class LacpConfig(BaseModel):
|
|
@@ -258,7 +257,7 @@ class LacpConfig(BaseModel):
|
|
|
258
257
|
|
|
259
258
|
class Lacp(BaseModel):
|
|
260
259
|
config: Optional[LacpConfig] = LacpConfig()
|
|
261
|
-
interfaces: Optional[Dict[str, Interface]] =
|
|
260
|
+
interfaces: Optional[Dict[str, Interface]] = None
|
|
262
261
|
|
|
263
262
|
# SNMP
|
|
264
263
|
class SnmpAccess(str, Enum):
|
|
@@ -323,7 +322,7 @@ class SnmpView(BaseModel):
|
|
|
323
322
|
|
|
324
323
|
|
|
325
324
|
class SnmpServer(BaseModel):
|
|
326
|
-
name: str
|
|
325
|
+
name: Optional[AttributeValue[str]] = None
|
|
327
326
|
address: AttributeValue[str]
|
|
328
327
|
port: Optional[AttributeValue[int]] = AttributeValue(value=162)
|
|
329
328
|
enabled: Optional[AttributeValue[bool]] = AttributeValue(value=True)
|
|
@@ -450,41 +449,41 @@ class TrapEventOptions(str, Enum):
|
|
|
450
449
|
BULKSTAT_TRANSFER = "bulkstat_transfer"
|
|
451
450
|
|
|
452
451
|
class TrapEvent(BaseModel):
|
|
453
|
-
name: str
|
|
454
|
-
event_name: TrapEventOptions
|
|
452
|
+
name: Optional[AttributeValue[str]] = None
|
|
453
|
+
event_name: Optional[AttributeValue[TrapEventOptions]] = None
|
|
455
454
|
|
|
456
455
|
#class SnmpTrap(BaseModel): ...
|
|
457
456
|
|
|
458
457
|
class Snmp(BaseModel):
|
|
459
|
-
config: Optional[Dict[str, SnmpConfig]] =
|
|
460
|
-
communities: Optional[Dict[str, SnmpCommunity]] =
|
|
461
|
-
users: Optional[Dict[str, SnmpUser]] =
|
|
462
|
-
trap_servers: Optional[Dict[str, SnmpServer]] =
|
|
463
|
-
trap_events: Optional[Dict[str, TrapEvent]] =
|
|
464
|
-
views: Optional[Dict[str, SnmpView]] =
|
|
458
|
+
config: Optional[Dict[str, SnmpConfig]] = None
|
|
459
|
+
communities: Optional[Dict[str, SnmpCommunity]] = None
|
|
460
|
+
users: Optional[Dict[str, SnmpUser]] = None
|
|
461
|
+
trap_servers: Optional[Dict[str, SnmpServer]] = None
|
|
462
|
+
trap_events: Optional[Dict[str, TrapEvent]] = None
|
|
463
|
+
views: Optional[Dict[str, SnmpView]] = None
|
|
465
464
|
|
|
466
465
|
# AAA
|
|
467
466
|
class aaaBaseClass(BaseModel):
|
|
468
|
-
name: str = None
|
|
467
|
+
name: Optional[AttributeValue[str]] = None
|
|
469
468
|
|
|
470
469
|
class aaaTacacsAttributes(BaseModel):
|
|
471
|
-
port: Optional[int] =
|
|
472
|
-
secret_key: Optional[str] = None
|
|
473
|
-
secret_key_hashed: Optional[str] = None
|
|
474
|
-
address: Optional[str] = None
|
|
475
|
-
timeout: Optional[int] =
|
|
476
|
-
source_interface: Optional[Reference] = None
|
|
470
|
+
port: Optional[AttributeValue[int]] = None
|
|
471
|
+
secret_key: Optional[AttributeValue[str]] = None
|
|
472
|
+
secret_key_hashed: Optional[AttributeValue[str]] = None
|
|
473
|
+
address: Optional[AttributeValue[str]] = None
|
|
474
|
+
timeout: Optional[AttributeValue[int]] = None
|
|
475
|
+
source_interface: Optional[Reference] = None
|
|
477
476
|
server_group: Optional[AttributeValue[str]] = None
|
|
478
477
|
|
|
479
478
|
class aaaRadiusAttributes(BaseModel):
|
|
480
|
-
auth_port: Optional[int] =
|
|
481
|
-
acct_port: Optional[int] =
|
|
482
|
-
secret_key: Optional[str] = None
|
|
483
|
-
secret_key_hashed: Optional[str] = None
|
|
484
|
-
address: Optional[str] = None
|
|
485
|
-
timeout: Optional[int] =
|
|
486
|
-
source_interface: Optional[Reference] = None
|
|
487
|
-
retransmit_attempts: Optional[int] =
|
|
479
|
+
auth_port: Optional[AttributeValue[int]] = None
|
|
480
|
+
acct_port: Optional[AttributeValue[int]] = None
|
|
481
|
+
secret_key: Optional[AttributeValue[str]] = None
|
|
482
|
+
secret_key_hashed: Optional[AttributeValue[str]] = None
|
|
483
|
+
address: Optional[AttributeValue[str]] = None
|
|
484
|
+
timeout: Optional[AttributeValue[int]] = None
|
|
485
|
+
source_interface: Optional[Reference] = None
|
|
486
|
+
retransmit_attempts: Optional[AttributeValue[int]] = None
|
|
488
487
|
server_group: Optional[AttributeValue[str]] = None
|
|
489
488
|
|
|
490
489
|
class aaaServerGroupAttributes(BaseModel):
|
|
@@ -494,13 +493,13 @@ class aaaServerGroupAttributes(BaseModel):
|
|
|
494
493
|
Type is used to tell future renderers what kind of server group this is.
|
|
495
494
|
Example:
|
|
496
495
|
type = 'tacacs' or type = 'radius'
|
|
497
|
-
|
|
496
|
+
|
|
498
497
|
The tacacs and radius attributes expect a reference to the aaaTacacs and aaaRadius models respectively.
|
|
499
498
|
|
|
500
499
|
Example in config map:
|
|
501
500
|
enable = True
|
|
502
501
|
type = 'tacacs'
|
|
503
|
-
tacacs = [tacacs_server1, tacacs_server2]
|
|
502
|
+
tacacs = [tacacs_server1, tacacs_server2]
|
|
504
503
|
radius = radius_server1
|
|
505
504
|
|
|
506
505
|
Cisco example:
|
|
@@ -508,10 +507,10 @@ class aaaServerGroupAttributes(BaseModel):
|
|
|
508
507
|
server name tacacs_server1
|
|
509
508
|
server name tacacs_server2
|
|
510
509
|
"""
|
|
511
|
-
enable: Optional[bool] =
|
|
512
|
-
type: Optional[Literal['tacacs','radius']] = None
|
|
513
|
-
tacacs: Optional[Dict[str, aaaTacacsAttributes]] =
|
|
514
|
-
radius: Optional[Dict[str, aaaRadiusAttributes]] =
|
|
510
|
+
enable: Optional[AttributeValue[bool]] = None
|
|
511
|
+
type: Optional[AttributeValue[Literal['tacacs','radius']]] = None
|
|
512
|
+
tacacs: Optional[Dict[str, aaaTacacsAttributes]] = None
|
|
513
|
+
radius: Optional[Dict[str, aaaRadiusAttributes]] = None
|
|
515
514
|
|
|
516
515
|
# Authentication Models
|
|
517
516
|
class aaaAuthenticationMethods(aaaBaseClass):
|
|
@@ -534,26 +533,26 @@ class aaaAuthenticationMethods(aaaBaseClass):
|
|
|
534
533
|
# aaa authentication enable default group TACACS-GROUP-NEW enable
|
|
535
534
|
|
|
536
535
|
class authenticationUser(aaaBaseClass):
|
|
537
|
-
username: Optional[str] = None
|
|
538
|
-
password: Optional[str] = None
|
|
539
|
-
password_hahsed: Optional[str] = None
|
|
540
|
-
ssh_key: Optional[str] = None
|
|
541
|
-
role: Optional[str] = None
|
|
536
|
+
username: Optional[AttributeValue[str]] = None
|
|
537
|
+
password: Optional[AttributeValue[str]] = None
|
|
538
|
+
password_hahsed: Optional[AttributeValue[str]] = None
|
|
539
|
+
ssh_key: Optional[AttributeValue[str]] = None
|
|
540
|
+
role: Optional[AttributeValue[str]] = None
|
|
542
541
|
|
|
543
542
|
class aaaAuthenticationUsers(aaaBaseClass):
|
|
544
|
-
username: Optional[Dict[str, authenticationUser]] =
|
|
543
|
+
username: Optional[Dict[str, authenticationUser]] = None
|
|
545
544
|
|
|
546
|
-
class adminUser(aaaBaseClass):
|
|
547
|
-
admin_password: Optional[str] = None
|
|
548
|
-
admin_password_hashed: Optional[str] = None
|
|
545
|
+
class adminUser(aaaBaseClass):
|
|
546
|
+
admin_password: Optional[AttributeValue[str]] = None
|
|
547
|
+
admin_password_hashed: Optional[AttributeValue[str]] = None
|
|
549
548
|
|
|
550
549
|
class aaaAuthenticationAdminUsers(BaseModel):
|
|
551
|
-
config: Optional[Dict[str, adminUser]] =
|
|
550
|
+
config: Optional[Dict[str, adminUser]] = None
|
|
552
551
|
|
|
553
552
|
class aaaAuthentication(BaseModel):
|
|
554
|
-
config: Optional[Dict[str, aaaAuthenticationMethods]] =
|
|
555
|
-
admin_user: Optional[Dict[str, aaaAuthenticationAdminUsers]] =
|
|
556
|
-
users: Optional[Dict[str, aaaAuthenticationUsers]] =
|
|
553
|
+
config: Optional[Dict[str, aaaAuthenticationMethods]] = None
|
|
554
|
+
admin_user: Optional[Dict[str, aaaAuthenticationAdminUsers]] = None
|
|
555
|
+
users: Optional[Dict[str, aaaAuthenticationUsers]] = None
|
|
557
556
|
|
|
558
557
|
# Authorization Models
|
|
559
558
|
class aaaAuthorizationMethods(aaaBaseClass):
|
|
@@ -583,10 +582,8 @@ class aaaAuthorizationEvents(aaaBaseClass):
|
|
|
583
582
|
event: Optional[AttributeValue[str]] = None
|
|
584
583
|
|
|
585
584
|
class aaaAuthorization(BaseModel):
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
#events: Optional[Dict[str, aaaAuthorizationEvents]] = {}
|
|
589
|
-
events: Optional[Dict[str, aaaAuthorizationEvents]] = {}
|
|
585
|
+
config: Optional[Dict[str, aaaAuthorizationMethods]] = None
|
|
586
|
+
events: Optional[Dict[str, aaaAuthorizationEvents]] = None
|
|
590
587
|
|
|
591
588
|
# Accounting Models
|
|
592
589
|
class aaaAccountingMethods(BaseModel):
|
|
@@ -614,17 +611,15 @@ class aaaAccountingEvents(BaseModel):
|
|
|
614
611
|
event: Optional[AttributeValue[str]] = None
|
|
615
612
|
|
|
616
613
|
class aaaAccounting(BaseModel):
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
#events: aaaAccountingEvents = aaaAccountingEvents()
|
|
620
|
-
events: Optional[Dict[str, aaaAccountingEvents]] = {}
|
|
614
|
+
config: Optional[Dict[str, aaaAccountingMethods]] = None
|
|
615
|
+
events: Optional[Dict[str, aaaAccountingEvents]] = None
|
|
621
616
|
|
|
622
617
|
class aaaGlobalAttributes(BaseModel):
|
|
623
618
|
enabled: Optional[AttributeValue[bool]] = False # default False
|
|
624
619
|
|
|
625
620
|
class TripleA(BaseModel):
|
|
626
621
|
config: aaaGlobalAttributes = aaaGlobalAttributes()
|
|
627
|
-
server_groups: Optional[Dict[str, aaaServerGroupAttributes]] =
|
|
622
|
+
server_groups: Optional[Dict[str, aaaServerGroupAttributes]] = None
|
|
628
623
|
authentication: aaaAuthentication = aaaAuthentication()
|
|
629
624
|
authorization: aaaAuthorization = aaaAuthorization()
|
|
630
625
|
accounting: aaaAccounting = aaaAccounting()
|
|
@@ -654,4 +649,14 @@ class ComposedConfiguration(BaseModel):
|
|
|
654
649
|
lacp: Optional[Lacp] = Lacp()
|
|
655
650
|
interfaces: Dict[str, InterfaceType] = {}
|
|
656
651
|
network_instances: Dict[str, NetworkInstance] = {"global": NetworkInstance(name="global")}
|
|
657
|
-
stp: Optional[SpanningTree] = SpanningTree()
|
|
652
|
+
stp: Optional[SpanningTree] = SpanningTree()
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
"""
|
|
656
|
+
GUIDELINES FOR COMPOSED CONFIGURATION:
|
|
657
|
+
|
|
658
|
+
1. All values must always be typed as AttributeValue.
|
|
659
|
+
2. Containers must always be defined as hierarchical pydantic types, no dicts as placeholders.
|
|
660
|
+
3. Component collections are always Dict[str, BaseModel] — the key identifies the component
|
|
661
|
+
4. Default values for Optional is always None.
|
|
662
|
+
"""
|
|
@@ -42,36 +42,36 @@ class LoggingConfig(BaseModel):
|
|
|
42
42
|
buffer_size: Optional[AttributeValue[int]] = None
|
|
43
43
|
|
|
44
44
|
class Console(BaseModel):
|
|
45
|
-
name: str = None
|
|
46
|
-
line_number: int = None
|
|
47
|
-
logging_synchronous: bool =
|
|
45
|
+
name: Optional[AttributeValue[str]] = None
|
|
46
|
+
line_number: Optional[AttributeValue[int]] = None
|
|
47
|
+
logging_synchronous: Optional[AttributeValue[bool]] = None
|
|
48
48
|
|
|
49
49
|
class RemoteServer(BaseModel):
|
|
50
|
-
name: str = None
|
|
51
|
-
host: str = None
|
|
52
|
-
port: Optional[int] =
|
|
53
|
-
transfer: Optional[str] =
|
|
50
|
+
name: Optional[AttributeValue[str]] = None
|
|
51
|
+
host: Optional[AttributeValue[str]] = None
|
|
52
|
+
port: Optional[AttributeValue[int]] = None
|
|
53
|
+
transfer: Optional[AttributeValue[str]] = None
|
|
54
54
|
source_address: Optional[AttributeValue[str]] = None # Can be an IP address or an interface reference
|
|
55
55
|
|
|
56
56
|
class VtyLine(BaseModel):
|
|
57
|
-
name: str = None
|
|
58
|
-
line_number: int = None
|
|
59
|
-
logging_synchronous: bool =
|
|
60
|
-
transport_input: Optional[str] =
|
|
57
|
+
name: Optional[AttributeValue[str]] = None
|
|
58
|
+
line_number: Optional[AttributeValue[int]] = None
|
|
59
|
+
logging_synchronous: Optional[AttributeValue[bool]] = None
|
|
60
|
+
transport_input: Optional[AttributeValue[str]] = None # default is SSH. Mostly used by Cisco.
|
|
61
61
|
|
|
62
62
|
class FileLogging(BaseModel):
|
|
63
|
-
name: str = None # object name
|
|
64
|
-
filename: str = None # name of the file
|
|
65
|
-
rotate: Optional[int] = None # How many versions to keep. Juniper specific.
|
|
66
|
-
max_size: Optional[int] = None # Max size in bytes. Used both for Cisco and Juniper.
|
|
67
|
-
min_size: Optional[int] = None # Min size in bytes. Only used for Cisco.
|
|
68
|
-
facility: LoggingFacility # Type of log
|
|
69
|
-
severity: LoggingSeverity # Severity level
|
|
63
|
+
name: Optional[AttributeValue[str]] = None # object name
|
|
64
|
+
filename: Optional[AttributeValue[str]] = None # name of the file
|
|
65
|
+
rotate: Optional[AttributeValue[int]] = None # How many versions to keep. Juniper specific.
|
|
66
|
+
max_size: Optional[AttributeValue[int]] = None # Max size in bytes. Used both for Cisco and Juniper.
|
|
67
|
+
min_size: Optional[AttributeValue[int]] = None # Min size in bytes. Only used for Cisco.
|
|
68
|
+
facility: Optional[AttributeValue[LoggingFacility]] = None # Type of log
|
|
69
|
+
severity: Optional[AttributeValue[LoggingSeverity]] = None # Severity level
|
|
70
70
|
|
|
71
71
|
class LoggingEvent(BaseModel):
|
|
72
|
-
enabled: bool
|
|
73
|
-
severity: LoggingSeverity
|
|
72
|
+
enabled: Optional[AttributeValue[bool]] = None
|
|
73
|
+
severity: Optional[AttributeValue[LoggingSeverity]] = None
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
class LoggingEvents(BaseModel):
|
|
77
|
-
events: Optional[Dict[str, LoggingEvent]] =
|
|
77
|
+
events: Optional[Dict[str, LoggingEvent]] = None
|
|
@@ -4,27 +4,27 @@ from enum import Enum
|
|
|
4
4
|
from typing import Optional, Dict
|
|
5
5
|
|
|
6
6
|
class SpanningTreeGlobalAttributes(BaseModel):
|
|
7
|
-
mode: Optional[str] = None # Needs to be defined by user. Default for Cisco is RAPID-PVST and for Juniper it's just RSTP
|
|
8
|
-
bpdu_filter: Optional[bool] =
|
|
9
|
-
bpdu_guard: Optional[bool] =
|
|
10
|
-
loop_guard: Optional[bool] =
|
|
11
|
-
portfast: Optional[bool] =
|
|
12
|
-
bridge_assurance: Optional[bool] =
|
|
13
|
-
#interfaces: Optional[Dict[str, Reference]] =
|
|
7
|
+
mode: Optional[AttributeValue[str]] = None # Needs to be defined by user. Default for Cisco is RAPID-PVST and for Juniper it's just RSTP
|
|
8
|
+
bpdu_filter: Optional[AttributeValue[bool]] = None # Disabled by default
|
|
9
|
+
bpdu_guard: Optional[AttributeValue[bool]] = None # Disabled by default
|
|
10
|
+
loop_guard: Optional[AttributeValue[bool]] = None # Disabled by default
|
|
11
|
+
portfast: Optional[AttributeValue[bool]] = None # Disabled by default. Global setting for access ports.
|
|
12
|
+
bridge_assurance: Optional[AttributeValue[bool]] = None # Disabled by default. Only supported by MST and PVRST+
|
|
13
|
+
#interfaces: Optional[Dict[str, Reference]] = None
|
|
14
14
|
|
|
15
15
|
class SpanningTreeModeConfig(BaseModel):
|
|
16
|
-
hello_time: Optional[int] = None
|
|
17
|
-
max_age: Optional[int] = None
|
|
18
|
-
forward_delay: Optional[int] = None
|
|
19
|
-
bridge_priority: Optional[int] = None
|
|
20
|
-
hold_count: Optional[int] = None # Range 1..10
|
|
16
|
+
hello_time: Optional[AttributeValue[int]] = None
|
|
17
|
+
max_age: Optional[AttributeValue[int]] = None
|
|
18
|
+
forward_delay: Optional[AttributeValue[int]] = None
|
|
19
|
+
bridge_priority: Optional[AttributeValue[int]] = None
|
|
20
|
+
hold_count: Optional[AttributeValue[int]] = None # Range 1..10
|
|
21
21
|
|
|
22
22
|
## RSTP
|
|
23
23
|
class RstpAttributes(SpanningTreeModeConfig): ...
|
|
24
24
|
|
|
25
25
|
class RSTPConfig(BaseModel):
|
|
26
26
|
#config: RstpAttributes = RstpAttributes()
|
|
27
|
-
config: Optional[Dict[str, RstpAttributes]] =
|
|
27
|
+
config: Optional[Dict[str, RstpAttributes]] = None
|
|
28
28
|
|
|
29
29
|
### MSTP
|
|
30
30
|
class MstpInstanceAttributes(SpanningTreeModeConfig):
|
|
@@ -38,8 +38,8 @@ class MstpAttributes(SpanningTreeModeConfig):
|
|
|
38
38
|
|
|
39
39
|
class MSTPConfig(BaseModel):
|
|
40
40
|
#config: MstpAttributes = MstpAttributes()
|
|
41
|
-
config: Optional[Dict[str, MstpAttributes]] =
|
|
42
|
-
mst_instances: Optional[Dict[str, MstpInstanceAttributes]] =
|
|
41
|
+
config: Optional[Dict[str, MstpAttributes]] = None
|
|
42
|
+
mst_instances: Optional[Dict[str, MstpInstanceAttributes]] = None
|
|
43
43
|
|
|
44
44
|
### Rapid PVST
|
|
45
45
|
class RapidPVSTAttributes(SpanningTreeModeConfig):
|
|
@@ -57,10 +57,10 @@ class RapidPVSTAttributes(SpanningTreeModeConfig):
|
|
|
57
57
|
vlan: Optional[AttributeValue[int | list[int]]] = None # Single VLAN ID or list of VLANs using Rapid PVST+
|
|
58
58
|
|
|
59
59
|
class RapidPVSTConfig(BaseModel):
|
|
60
|
-
vlan: Optional[Dict[str, RapidPVSTAttributes]] =
|
|
60
|
+
vlan: Optional[Dict[str, RapidPVSTAttributes]] = None
|
|
61
61
|
|
|
62
62
|
class SpanningTree(BaseModel):
|
|
63
|
-
config: Optional[Dict[str, SpanningTreeGlobalAttributes]] =
|
|
63
|
+
config: Optional[Dict[str, SpanningTreeGlobalAttributes]] = None#SpanningTreeGlobalAttributes()
|
|
64
64
|
rstp: Optional[RSTPConfig] = RSTPConfig()
|
|
65
65
|
mstp: Optional[MSTPConfig] = MSTPConfig()
|
|
66
66
|
rapidpvst: Optional[RapidPVSTConfig] = RapidPVSTConfig()
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
from typing import Any, Dict, List, Iterator, Tuple
|
|
3
|
-
|
|
4
|
-
from acex_devkit.models.composed_configuration import ComposedConfiguration, Reference, ReferenceTo, ReferenceFrom, Metadata
|
|
5
|
-
from acex_devkit.models.attribute_value import AttributeValue
|
|
6
|
-
from acex_devkit.configdiffer.diff import Diff, ComponentChange, AttributeChange, ComponentDiffOp
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ConfigDiffer:
|
|
12
|
-
|
|
13
|
-
IGNORED_KEYS = ["metadata", "logging", "ssh", "snmp", "network_instances", "interfaces", "ntp", "domain_name", "location", "contact", "motd_banner"]
|
|
14
|
-
|
|
15
|
-
def _clean_metadata(self, obj: dict) -> dict:
|
|
16
|
-
"""
|
|
17
|
-
Recursively ignored keys.
|
|
18
|
-
"""
|
|
19
|
-
if not isinstance(obj, dict):
|
|
20
|
-
return obj
|
|
21
|
-
cleaned = {}
|
|
22
|
-
for k, v in obj.items():
|
|
23
|
-
if k in self.__class__.IGNORED_KEYS:
|
|
24
|
-
continue
|
|
25
|
-
if isinstance(v, dict):
|
|
26
|
-
cleaned[k] = self._clean_metadata(v)
|
|
27
|
-
elif isinstance(v, list):
|
|
28
|
-
cleaned[k] = [self._clean_metadata(item) if isinstance(item, dict) else item for item in v]
|
|
29
|
-
else:
|
|
30
|
-
cleaned[k] = v
|
|
31
|
-
return cleaned
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _dump_to_dicts(self, config: ComposedConfiguration) -> dict:
|
|
35
|
-
"""
|
|
36
|
-
Dumps to dict, removes unnecessary keys except component_class.
|
|
37
|
-
"""
|
|
38
|
-
# Remove most metadata, but keep component_class for type identification
|
|
39
|
-
config_dict = config.model_dump(exclude_unset=True)
|
|
40
|
-
return self._clean_metadata(config_dict)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _flatten(self, d: dict, path: tuple = ()) -> Dict[tuple, dict]:
|
|
44
|
-
"""
|
|
45
|
-
Recursively walk the tree and collect every component (dict with
|
|
46
|
-
'component_class') into a flat mapping of path → component_dict.
|
|
47
|
-
|
|
48
|
-
Example result:
|
|
49
|
-
{
|
|
50
|
-
('interfaces', 'GigabitEthernet0/0/1'): {...},
|
|
51
|
-
('network_instances', 'default', 'vlans', '100'): {...},
|
|
52
|
-
}
|
|
53
|
-
"""
|
|
54
|
-
result = {}
|
|
55
|
-
for key, value in d.items():
|
|
56
|
-
current_path = path + (key,)
|
|
57
|
-
if not isinstance(value, dict):
|
|
58
|
-
continue
|
|
59
|
-
# Try to find deeper components first
|
|
60
|
-
deeper = self._flatten(value, current_path)
|
|
61
|
-
if deeper:
|
|
62
|
-
# Sub-components found — use those (don't emit current level)
|
|
63
|
-
result.update(deeper)
|
|
64
|
-
elif any(isinstance(v, dict) and 'value' in v for v in value.values()):
|
|
65
|
-
# No sub-components, but this dict has AttributeValue fields → it's a leaf
|
|
66
|
-
result[current_path] = value
|
|
67
|
-
return result
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _get_by_path(self, config: ComposedConfiguration, path: tuple) -> Any:
|
|
72
|
-
"""
|
|
73
|
-
Traverse the ComposedConfiguration object using a path tuple and return
|
|
74
|
-
the actual Pydantic model instance (or primitive) at that location.
|
|
75
|
-
|
|
76
|
-
Example:
|
|
77
|
-
path = ('interfaces', 'GigabitEthernet0/0/1')
|
|
78
|
-
→ returns the actual SoftwareLoopbackInterface / EthernetCsmacdInterface etc.
|
|
79
|
-
"""
|
|
80
|
-
obj = config
|
|
81
|
-
for key in path:
|
|
82
|
-
if isinstance(obj, dict):
|
|
83
|
-
obj = obj[key]
|
|
84
|
-
else:
|
|
85
|
-
obj = getattr(obj, key)
|
|
86
|
-
return obj
|
|
87
|
-
|
|
88
|
-
def _attribute_changes(self, before: dict, after: dict) -> List[AttributeChange]:
|
|
89
|
-
"""
|
|
90
|
-
Compare two component dicts and return a list of changed attributes.
|
|
91
|
-
"""
|
|
92
|
-
changes = []
|
|
93
|
-
for key in set(before.keys()) | set(after.keys()):
|
|
94
|
-
b = before.get(key)
|
|
95
|
-
a = after.get(key)
|
|
96
|
-
if b != a:
|
|
97
|
-
changes.append(AttributeChange(attribute_name=key, before=b, after=a))
|
|
98
|
-
return changes
|
|
99
|
-
|
|
100
|
-
def diff(self, *, desired_config: ComposedConfiguration, observed_config: ComposedConfiguration) -> Diff:
|
|
101
|
-
"""
|
|
102
|
-
Compare two ComposedConfiguration objects and return a component-based diff.
|
|
103
|
-
|
|
104
|
-
Walks both configs using Pydantic model structure (not heuristics) to find
|
|
105
|
-
all named components inside Dict[str, Model] containers. Components are
|
|
106
|
-
identified by their full path, e.g. ('interfaces', 'GigabitEthernet0/0/1').
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
desired_config: The target configuration we want to achieve.
|
|
110
|
-
observed_config: The current configuration as observed on the device.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
Diff: A structured diff showing added, removed, and changed components.
|
|
114
|
-
"""
|
|
115
|
-
flat_observed = self._flatten(self._dump_to_dicts(observed_config))
|
|
116
|
-
flat_desired = self._flatten(self._dump_to_dicts(desired_config))
|
|
117
|
-
|
|
118
|
-
observed_paths = set(flat_observed.keys())
|
|
119
|
-
desired_paths = set(flat_desired.keys())
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
added = []
|
|
123
|
-
removed = []
|
|
124
|
-
changed = []
|
|
125
|
-
|
|
126
|
-
for path in desired_paths - observed_paths:
|
|
127
|
-
obj = self._get_by_path(desired_config, path)
|
|
128
|
-
added.append(ComponentChange(
|
|
129
|
-
op=ComponentDiffOp.ADD,
|
|
130
|
-
path=list(path),
|
|
131
|
-
component_type=type(obj),
|
|
132
|
-
component_name=path[-1],
|
|
133
|
-
before=None,
|
|
134
|
-
after=obj,
|
|
135
|
-
before_dict=None,
|
|
136
|
-
after_dict=flat_desired[path],
|
|
137
|
-
))
|
|
138
|
-
|
|
139
|
-
for path in observed_paths - desired_paths:
|
|
140
|
-
obj = self._get_by_path(observed_config, path)
|
|
141
|
-
removed.append(ComponentChange(
|
|
142
|
-
op=ComponentDiffOp.REMOVE,
|
|
143
|
-
path=list(path),
|
|
144
|
-
component_type=type(obj),
|
|
145
|
-
component_name=path[-1],
|
|
146
|
-
before=obj,
|
|
147
|
-
after=None,
|
|
148
|
-
before_dict=flat_observed[path],
|
|
149
|
-
after_dict=None,
|
|
150
|
-
))
|
|
151
|
-
|
|
152
|
-
for path in desired_paths & observed_paths:
|
|
153
|
-
desired_comp = flat_desired[path]
|
|
154
|
-
observed_comp = flat_observed[path]
|
|
155
|
-
if desired_comp != observed_comp:
|
|
156
|
-
desired_obj = self._get_by_path(desired_config, path)
|
|
157
|
-
observed_obj = self._get_by_path(observed_config, path)
|
|
158
|
-
changed.append(ComponentChange(
|
|
159
|
-
op=ComponentDiffOp.CHANGE,
|
|
160
|
-
path=list(path),
|
|
161
|
-
component_type=type(desired_obj),
|
|
162
|
-
component_name=path[-1],
|
|
163
|
-
before=observed_obj,
|
|
164
|
-
after=desired_obj,
|
|
165
|
-
before_dict=observed_comp,
|
|
166
|
-
after_dict=desired_comp,
|
|
167
|
-
changed_attributes=self._attribute_changes(observed_comp, desired_comp),
|
|
168
|
-
))
|
|
169
|
-
|
|
170
|
-
return Diff(
|
|
171
|
-
added=added,
|
|
172
|
-
removed=removed,
|
|
173
|
-
changed=changed,
|
|
174
|
-
total_desired=len(desired_paths),
|
|
175
|
-
total_observed=len(observed_paths),
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
|
|
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
|