acex-devkit 1.0.0__tar.gz → 1.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acex-devkit
3
- Version: 1.0.0
3
+ Version: 1.0.2
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
@@ -11,6 +11,7 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: deepdiff (>=8.6.1,<9.0.0)
14
15
  Requires-Dist: pydantic (>=2.12.5,<3.0.0)
15
16
  Requires-Dist: typing-extensions (>=4.0.0,<5.0.0)
16
17
  Project-URL: Homepage, https://github.com/acex-labs/acex
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "acex-devkit"
3
- version = "1.0.0"
3
+ version = "1.0.2"
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"
@@ -16,6 +16,7 @@ packages = [
16
16
  python = "^3.13"
17
17
  pydantic = "^2.12.5"
18
18
  typing-extensions = "^4.0.0"
19
+ deepdiff = "^8.6.1"
19
20
 
20
21
  [tool.poetry.group.dev.dependencies]
21
22
  pytest = "^8.0.0"
@@ -0,0 +1,2 @@
1
+
2
+ from .configdiffer import Diff, ConfigDiffer, DiffNode, DiffOp
@@ -0,0 +1,230 @@
1
+ from enum import Enum
2
+ from typing import Any, Dict, Optional
3
+ from pydantic import BaseModel, model_validator
4
+ from deepdiff import DeepDiff
5
+
6
+
7
+ class DiffOp(str, Enum):
8
+ ADD = "add"
9
+ REMOVE = "remove"
10
+ CHANGE = "change"
11
+
12
+
13
+ class DiffNode(BaseModel):
14
+ op: DiffOp
15
+ before: Optional[Any] = None
16
+ after: Optional[Any] = None
17
+ children: Optional[Dict[str, "DiffNode"]] = None
18
+
19
+ @model_validator(mode="after")
20
+ def validate_invariants(self):
21
+ if self.op == DiffOp.ADD:
22
+ assert self.before is None and self.after is not None
23
+ elif self.op == DiffOp.REMOVE:
24
+ assert self.before is not None and self.after is None
25
+ elif self.op == DiffOp.CHANGE:
26
+ assert self.before is not None and self.after is not None
27
+ return self
28
+
29
+
30
+ class Diff(BaseModel):
31
+ root: DiffNode
32
+
33
+ def is_empty(self) -> bool:
34
+ return not bool(self.root.children)
35
+
36
+ def summary(self) -> dict[str, int]:
37
+ stats = {"add": 0, "remove": 0, "change": 0}
38
+
39
+ def walk(node: DiffNode):
40
+ stats[node.op.value] += 1
41
+ if node.children:
42
+ for child in node.children.values():
43
+ walk(child)
44
+
45
+ walk(self.root)
46
+ return stats
47
+
48
+
49
+
50
+ class ConfigDiffer:
51
+
52
+ def diff(self, *, desired_config: dict, observed_config: dict) -> Diff:
53
+ children = self._diff_dicts(
54
+ desired=desired_config,
55
+ observed=observed_config,
56
+ )
57
+
58
+ root = DiffNode(
59
+ op=DiffOp.CHANGE,
60
+ before=observed_config,
61
+ after=desired_config,
62
+ children=children or None,
63
+ )
64
+
65
+ return Diff(root=root)
66
+
67
+ def _remove_keys(self, obj: dict, keys_to_remove: list[str]) -> dict:
68
+ cleaned = {}
69
+
70
+ for k, v in obj.items():
71
+ if k in keys_to_remove:
72
+ continue
73
+
74
+ if isinstance(v, dict):
75
+ cleaned[k] = self._remove_keys(v, keys_to_remove)
76
+ else:
77
+ cleaned[k] = v
78
+
79
+ return cleaned
80
+
81
+ def _diff_dicts(
82
+ self,
83
+ *,
84
+ desired: dict,
85
+ observed: dict,
86
+ ) -> Dict[str, DiffNode]:
87
+ """
88
+ Diff two dicts by traversing down to 'value' attributes and comparing them.
89
+ Returns a hierarchical structure of DiffNodes.
90
+ """
91
+
92
+ # Remove metadata:
93
+ ignored_keys = ["metadata"]
94
+ observed = self._remove_keys(observed.model_dump(), ignored_keys)
95
+ desired = self._remove_keys(desired.model_dump(), ignored_keys)
96
+
97
+ # Diff
98
+ diff = DeepDiff(
99
+ observed,
100
+ desired
101
+ )
102
+
103
+ result: Dict[str, DiffNode] = {}
104
+
105
+ # Handle added items
106
+ items_added = diff.get("dictionary_item_added", set())
107
+ for path in items_added:
108
+ keys = self._parse_path(path)
109
+ if keys:
110
+ value = self._get_nested_value(desired, keys)
111
+ self._add_to_result(result, keys, DiffNode(
112
+ op=DiffOp.ADD,
113
+ after=value
114
+ ))
115
+
116
+ # Handle removed items
117
+ items_removed = diff.get("dictionary_item_removed", set())
118
+ for path in items_removed:
119
+ keys = self._parse_path(path)
120
+ if keys:
121
+ value = self._get_nested_value(observed, keys)
122
+ self._add_to_result(result, keys, DiffNode(
123
+ op=DiffOp.REMOVE,
124
+ before=value
125
+ ))
126
+
127
+ # Handle changed values
128
+ values_changed = diff.get("values_changed", {})
129
+ for path, change in values_changed.items():
130
+ keys = self._parse_path(path)
131
+ if keys:
132
+ self._add_to_result(result, keys, DiffNode(
133
+ op=DiffOp.CHANGE,
134
+ before=change["old_value"],
135
+ after=change["new_value"]
136
+ ))
137
+
138
+ return result
139
+
140
+ def _parse_path(self, path: str) -> list[str]:
141
+ """Parse DeepDiff path like "root['key1']['key2']" into ['key1', 'key2']"""
142
+ import re
143
+ matches = re.findall(r"\['([^']+)'\]|\[\"([^\"]+)\"\]", path)
144
+ return [m[0] or m[1] for m in matches]
145
+
146
+ def _get_nested_value(self, obj: dict, keys: list[str]) -> Any:
147
+ """Get value from nested dict using list of keys"""
148
+ current = obj
149
+ for key in keys:
150
+ if isinstance(current, dict):
151
+ current = current.get(key)
152
+ else:
153
+ return None
154
+ return current
155
+
156
+ def _add_to_result(self, result: Dict[str, DiffNode], keys: list[str], node: DiffNode):
157
+ """Add a DiffNode to the result dict at the appropriate nested location"""
158
+ if not keys:
159
+ return
160
+
161
+ if len(keys) == 1:
162
+ # Leaf node
163
+ result[keys[0]] = node
164
+ else:
165
+ # Need to create intermediate nodes
166
+ if keys[0] not in result:
167
+ # Create a CHANGE node as intermediate
168
+ result[keys[0]] = DiffNode(
169
+ op=DiffOp.CHANGE,
170
+ before={},
171
+ after={},
172
+ children={}
173
+ )
174
+
175
+ # Ensure it has children dict
176
+ if result[keys[0]].children is None:
177
+ result[keys[0]].children = {}
178
+
179
+ # Recursively add to children
180
+ self._add_to_result(result[keys[0]].children, keys[1:], node)
181
+
182
+ def apply_diff(self, config, diff: Diff):
183
+ """
184
+ Apply a diff to a configuration.
185
+
186
+ Args:
187
+ config: The base configuration (ComposedConfiguration or dict)
188
+ diff: The diff to apply
189
+
190
+ Returns:
191
+ A new configuration of the same type with the diff applied
192
+ """
193
+ import copy
194
+ from acex_devkit.models.composed_configuration import ComposedConfiguration
195
+
196
+ # Convert to dict if it's a ComposedConfiguration
197
+ is_composed = isinstance(config, ComposedConfiguration)
198
+ config_dict = config.model_dump(mode="python") if is_composed else config
199
+
200
+ # Deep copy to avoid modifying the original
201
+ result = copy.deepcopy(config_dict)
202
+
203
+ def apply_node(target: dict, node: DiffNode, key: str):
204
+ """Apply a single diff node to the target dict."""
205
+ if node.op == DiffOp.ADD:
206
+ target[key] = node.after
207
+ elif node.op == DiffOp.REMOVE:
208
+ if key in target:
209
+ del target[key]
210
+ elif node.op == DiffOp.CHANGE:
211
+ if node.children:
212
+ # Has children - recurse deeper
213
+ if key not in target:
214
+ target[key] = {}
215
+ for child_key, child_node in node.children.items():
216
+ apply_node(target[key], child_node, child_key)
217
+ else:
218
+ # Leaf value change
219
+ target[key] = node.after
220
+
221
+ # Apply all root-level changes
222
+ if diff.root.children:
223
+ for key, node in diff.root.children.items():
224
+ apply_node(result, node, key)
225
+
226
+ # Return same type as input
227
+ if is_composed:
228
+ return ComposedConfiguration(**result)
229
+ return result
230
+
@@ -3,8 +3,6 @@
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import Any, Dict
5
5
 
6
- from acex_devkit.models.logical_node import LogicalNode
7
-
8
6
 
9
7
  class ParserBase(ABC):
10
8
  """Base class for configuration parsers."""
@@ -98,7 +96,7 @@ class NetworkElementDriver:
98
96
  self.parser = self.parser_class()
99
97
 
100
98
  @abstractmethod
101
- def render(self, logical_node: LogicalNode, asset: Any = None) -> Any:
99
+ def render(self, logical_node: "LogicalNode", asset: Any = None) -> Any:
102
100
  """Render logical node to device configuration.
103
101
 
104
102
  Args:
@@ -39,15 +39,14 @@ class NetworkElementDriver:
39
39
  self.parser = self.parser_class()
40
40
 
41
41
  @abstractmethod
42
- def render(self, logical_node: "LogicalNode") -> Any:
43
- """Tar en LogicalNode och returnerar en konfigurationsrepresentation."""
44
- return self.renderer.render(logical_node.model_dump())
45
-
46
-
47
- # def apply(self, model: Dict[str, Any]) -> None:
48
- # cfg = self.renderer.render(model)
49
- # self.transport.connect()
50
- # self.transport.send(cfg)
51
- # if not self.transport.verify():
52
- # self.transport.rollback()
53
- # raise RuntimeError("Verification failed – rollback executed")
42
+ def render(self, configuration: ComposedConfiguration, asset: "Asset") -> Any:
43
+ """Render configuration from composedconfig and asset."""
44
+ pass
45
+
46
+ @abstractmethod
47
+ def parse(self, configuration: str) -> ComposedConfiguration:
48
+ """
49
+ Parse observed configuration into a composedconfiguration object.
50
+ """
51
+
52
+
@@ -3,5 +3,7 @@
3
3
  # TODO: Move all models to this package
4
4
  # TODO: Move all base classes to this package
5
5
 
6
+ from .external_value import ExternalValue
7
+ from .attribute_value import AttributeValue
6
8
 
7
9
  __all__ = []
@@ -0,0 +1,108 @@
1
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator, field_serializer
2
+ from typing import Union, TypeVar, Generic, Optional, Dict, Any, get_args, get_origin
3
+ from acex_devkit.models import ExternalValue
4
+
5
+ T = TypeVar('T')
6
+
7
+ class AttributeValue(BaseModel, Generic[T]):
8
+ """
9
+ A generic wrapper for values that may be concrete or external.
10
+ """
11
+ model_config = ConfigDict(arbitrary_types_allowed=True)
12
+
13
+ value: Union[T, ExternalValue]
14
+ metadata: Optional[Dict[str, Any]] = None
15
+
16
+ # ---------------------------------------------------------
17
+ # 1) PRE-PROCESSOR: Tillåt råa värden, ExternalValue direkt,
18
+ # eller dict → AttributeValue
19
+ # ---------------------------------------------------------
20
+ @model_validator(mode="before")
21
+ @classmethod
22
+ def preprocess_raw(cls, data):
23
+ """
24
+ Gör varje typ av input till en fullvärdig {"value": ...} dict
25
+ så att Pydantic kan fortsätta normalt.
26
+ """
27
+ # 1) Rått värde (str, int, ExternalValue etc)
28
+ if not isinstance(data, dict):
29
+ return {"value": data}
30
+
31
+ # 2) Dict som representerar ExternalValue
32
+ if "value" not in data and "ref" in data:
33
+ return {"value": ExternalValue(**data)}
34
+
35
+ # 3) Dict som redan har value → låt vara
36
+ return data
37
+
38
+ # ---------------------------------------------------------
39
+ # 2) VALIDATOR för value
40
+ # ---------------------------------------------------------
41
+ @field_validator("value", mode="before")
42
+ @classmethod
43
+ def normalize_value(cls, v):
44
+ if isinstance(v, cls):
45
+ return v.value
46
+ if isinstance(v, ExternalValue):
47
+ return v
48
+ if isinstance(v, dict) and "ref" in v:
49
+ return ExternalValue(**v)
50
+ return v
51
+
52
+ # ---------------------------------------------------------
53
+ # 3) METADATA-generator (after)
54
+ # Bevarar redan satt metadata!
55
+ # ---------------------------------------------------------
56
+ @model_validator(mode="after")
57
+ def set_automatic_metadata(self):
58
+ """
59
+ Lägg till metadata utan att ta bort användarens egna.
60
+ """
61
+ self.metadata = dict(self.metadata or {}) # KOPIA – ändra inte originalet
62
+
63
+ if isinstance(self.value, ExternalValue):
64
+ self.metadata.setdefault("value_type", "external")
65
+ self.metadata.setdefault("attr_ptr", self.value.attr_ptr)
66
+ self.metadata.setdefault("plugin", self.value.plugin)
67
+ self.metadata.setdefault(
68
+ "ev_type",
69
+ self.value.ev_type.value
70
+ if hasattr(self.value.ev_type, "value")
71
+ else self.value.ev_type,
72
+ )
73
+ self.metadata.setdefault("query", self.value.query)
74
+ self.metadata.setdefault("resolved", self.value.resolved)
75
+
76
+ if self.value.kind:
77
+ self.metadata.setdefault("kind", self.value.kind)
78
+
79
+ if self.value.resolved and self.value.resolved_at:
80
+ self.metadata.setdefault(
81
+ "resolved_at",
82
+ self.value.resolved_at.isoformat()
83
+ if hasattr(self.value.resolved_at, "isoformat")
84
+ else str(self.value.resolved_at),
85
+ )
86
+
87
+ else:
88
+ self.metadata.setdefault("value_type", "concrete")
89
+ self.metadata.setdefault("type", type(self.value).__name__)
90
+
91
+ return self
92
+
93
+ # ---------------------------------------------------------
94
+ # 4) SERIALIZER – external value → returnera dess "value"
95
+ # ---------------------------------------------------------
96
+ @field_serializer("value")
97
+ def serialize_value(self, value):
98
+ if isinstance(value, ExternalValue):
99
+ return value.value
100
+ return value
101
+
102
+ # Convenience helpers
103
+ def is_external(self) -> bool:
104
+ return isinstance(self.value, ExternalValue)
105
+
106
+ def get_value(self) -> T:
107
+ return self.value.value if isinstance(self.value, ExternalValue) else self.value
108
+
@@ -0,0 +1,579 @@
1
+
2
+ from pydantic import BaseModel, Field
3
+ from typing import Optional, Dict, List, Literal, ClassVar, Union, Any
4
+ from enum import Enum
5
+
6
+ from acex_devkit.models.external_value import ExternalValue
7
+ from acex_devkit.models.attribute_value import AttributeValue
8
+ from acex_devkit.models.logging import (
9
+ LoggingConfig,
10
+ Console,
11
+ RemoteServer,
12
+ VtyLine,
13
+ FileLogging,
14
+ LoggingEvents
15
+ )
16
+ from acex_devkit.models.spanning_tree import SpanningTree
17
+
18
+ class MetadataValueType(str, Enum):
19
+ CONCRETE = "concrete"
20
+ EXTERNALVALUE = "externalValue"
21
+ REFERENCE = "reference"
22
+
23
+ class Metadata(BaseModel):
24
+ type: Optional[str] = "str"
25
+ value_source: MetadataValueType = MetadataValueType.CONCRETE
26
+
27
+ class Reference(BaseModel):
28
+ pointer: str
29
+ metadata: Metadata = Metadata(type="str", value_source="reference")
30
+
31
+ class ReferenceTo(Reference):
32
+ pointer: str
33
+ metadata: Optional[Dict] = {}
34
+
35
+ class ReferenceFrom(Reference):
36
+ pointer: str
37
+ metadata: Optional[Dict] = {}
38
+
39
+ class RenderedReference(BaseModel):
40
+ from_ptr: str
41
+ to_ptr: str
42
+
43
+ class SystemConfig(BaseModel):
44
+ contact: Optional[AttributeValue[str]] = None
45
+ domain_name: Optional[AttributeValue[str]] = None
46
+ hostname: Optional[AttributeValue[str]] = None
47
+ location: Optional[AttributeValue[str]] = None
48
+
49
+ class TripleA(BaseModel): ...
50
+
51
+ # Trying to avoid using "Logging" or "logging" as names for anything due to conflicts with standard lib.
52
+ class LoggingComponents(BaseModel):
53
+ config: LoggingConfig = LoggingConfig()
54
+ console: Optional[Console] = None
55
+ remote_servers: Optional[Dict[str, RemoteServer]] = {}
56
+ events: Optional[LoggingEvents] = None
57
+ vty: Optional[Dict[str, VtyLine]] = {}
58
+ files: Optional[Dict[str, FileLogging]] = {}
59
+
60
+ class NtpConfig(BaseModel):
61
+ enabled: AttributeValue[bool] = AttributeValue(value=False)
62
+
63
+ class NtpServer(BaseModel):
64
+ address: AttributeValue[str]
65
+ port: Optional[AttributeValue[int]] = None
66
+ version: Optional[AttributeValue[int]] = None
67
+ association_typ: Optional[AttributeValue[str]] = None
68
+ prefer: Optional[AttributeValue[bool]] = None
69
+ source_interface: Optional[AttributeValue[str]] = None
70
+
71
+ class Ntp(BaseModel):
72
+ config: Optional[NtpConfig] = None
73
+ servers: Optional[Dict[str, NtpServer]] = {}
74
+
75
+ class SshServer(BaseModel):
76
+ enable: Optional[AttributeValue[bool]] = None
77
+ protocol_version: Optional[AttributeValue[int]] = AttributeValue(value=2)
78
+ timeout: Optional[AttributeValue[int]] = None
79
+ auth_retries: Optional[AttributeValue[int]] = None
80
+ source_interface: Optional[Reference] = None
81
+
82
+ class AuthorizedKeyAlgorithms(str, Enum):
83
+ SSH_ED25519 = "ssh-ed25519"
84
+ ECDSA_NISTP256 = "ecdsa-sha2-nistp256"
85
+ ECDSA_NISTP384 = "ecdsa-sha2-nistp384"
86
+ ECDSA_NISTP521 = "ecdsa-sha2-nistp521"
87
+ RSA_SHA2_256 = "rsa-sha2-256"
88
+ RSA_SHA2_512 = "rsa-sha2-512"
89
+ SK_SSH_ED25519 = "sk-ssh-ed25519@openssh.com"
90
+ SK_ECDSA_NISTP256 = "sk-ecdsa-sha2-nistp256@openssh.com"
91
+ SSH_RSA = "ssh-rsa"
92
+ SSH_DSS = "ssh-dss"
93
+
94
+ class AuthorizedKey(BaseModel):
95
+ algorithm: AuthorizedKeyAlgorithms
96
+ public_key: str
97
+
98
+ class Ssh(BaseModel):
99
+ config: Optional[SshServer] = None
100
+ host_keys: Optional[Dict[str, AuthorizedKey]] = {}
101
+
102
+ class Acl(BaseModel): ...
103
+ class Lldp(BaseModel): ...
104
+
105
+ class Vlan(BaseModel):
106
+ name: AttributeValue[str]
107
+ vlan_id: Optional[AttributeValue[int]] = None
108
+ vlan_name: Optional[AttributeValue[str]] = None
109
+ network_instance: Optional[AttributeValue[str]] = None
110
+ metadata: Optional[Metadata] = Metadata()
111
+
112
+ class Interface(BaseModel):
113
+ "Base class for all interfaces"
114
+ index: AttributeValue[int]
115
+ name: AttributeValue[str]
116
+
117
+ description: Optional[AttributeValue[str]] = None
118
+ enabled: Optional[AttributeValue[bool]] = None
119
+ ipv4: Optional[AttributeValue[str]] = None
120
+
121
+ metadata: Optional[Metadata] = Metadata()
122
+ type: Literal[
123
+ "ethernetCsmacd",
124
+ "ieee8023adLag",
125
+ "l3ipvlan",
126
+ "softwareLoopback",
127
+ "subinterface",
128
+ "managementInterface"
129
+ ] = "ethernetCsmacd"
130
+
131
+ model_config = {
132
+ "discriminator": "type"
133
+ }
134
+
135
+
136
+ class EthernetCsmacdInterface(Interface):
137
+ "Physical Interface"
138
+ type: Literal["ethernetCsmacd"] = "ethernetCsmacd"
139
+
140
+ # Egenskaper för fysiska interface
141
+ stack_index: Optional[AttributeValue[int]] = None
142
+ module_index: Optional[AttributeValue[int]] = None
143
+ subinterfaces: list["SubInterface"] = Field(default_factory=list)
144
+ speed: Optional[AttributeValue[int]] = None
145
+ duplex: Optional[AttributeValue[str]] = None
146
+ switchport: Optional[AttributeValue[bool]] = None
147
+ switchport_mode: Optional[AttributeValue[Literal["access", "trunk"]]] = None
148
+ trunk_allowed_vlans: Optional[AttributeValue[List[int]]] = None
149
+ native_vlan: Optional[AttributeValue[int]] = None
150
+ access_vlan: Optional[AttributeValue[int]] = None
151
+ vlan_id: Optional[AttributeValue[int]] = None
152
+ voice_vlan: Optional[AttributeValue[int]] = None
153
+ mtu: Optional[AttributeValue[int]] = None # No default set as it differs between devices and vendors
154
+ negotiation: Optional[AttributeValue[bool]] = None
155
+
156
+ # LACP relaterade attribut
157
+ aggregate_id: Optional[AttributeValue[int]] = None
158
+ lacp_enabled: Optional[AttributeValue[bool]] = None
159
+ lacp_mode: Optional[AttributeValue[Literal["active", "passive", "on", "auto"]]] = None
160
+ lacp_port_priority: Optional[AttributeValue[int]] = None
161
+ #lacp_system_id_mac: Optional[AttributeValue[str]] = None
162
+ lacp_interval: Optional[AttributeValue[Literal["fast", "slow"]]] = None
163
+
164
+ # Spanning-tree relaterade attribut
165
+ stp_port_priority: Optional[int] = None
166
+ stp_cost: Optional[int] = None
167
+ stp_edge_port: Optional[bool] = False # Disabled by default
168
+ stp_bpdu_filter: Optional[bool] = False # Disabled by default
169
+ stp_bpdu_guard: Optional[bool] = False # Disabled by default
170
+ stp_loop_guard: Optional[bool] = False # Disabled by default
171
+ stp_root_guard: Optional[bool] = False # Disabled by default
172
+ stp_portfast: Optional[bool] = False # Disabled by default
173
+ stp_link_type: Optional[Literal["point-to-point", "shared"]] = None # e.g., "point-to-point", "shared"
174
+
175
+
176
+ class Ieee8023adLagInterface(Interface):
177
+ "LAG Interface"
178
+ type: Literal["ieee8023adLag"] = "ieee8023adLag"
179
+ #aggregate_id: AttributeValue[int] = None
180
+ aggregate_id: int = None
181
+ members: list[str] = Field(default_factory=list)
182
+ max_ports: Optional[AttributeValue[int]] = None
183
+ switchport: Optional[AttributeValue[bool]] = None
184
+ switchport_mode: Optional[AttributeValue[Literal["access", "trunk"]]] = None
185
+ trunk_allowed_vlans: Optional[AttributeValue[List[int]]] = None
186
+ native_vlan: Optional[AttributeValue[int]] = None
187
+ mtu: Optional[AttributeValue[int]] = None # No default set as it differs between devices and vendors
188
+
189
+ class L3IpvlanInterface(Interface):
190
+ "SVI Interface"
191
+ type: Literal["l3ipvlan"] = "l3ipvlan"
192
+ vlan_id: Optional[int] = None
193
+
194
+ class SoftwareLoopbackInterface(Interface):
195
+ "Loopback Interface"
196
+ type: Literal["softwareLoopback"] = "softwareLoopback"
197
+
198
+ # Loopback har varken vlan, duplex eller speed
199
+ vlan_id: Optional[int] = None
200
+ ipv4: Optional[AttributeValue[str]] = None
201
+
202
+ class SubInterface(Interface):
203
+ "Subinterface"
204
+ type: Literal["subinterface"] = "subinterface"
205
+
206
+ vlan_id: Optional[int] = None
207
+ ipv4: Optional[AttributeValue[str]] = None
208
+
209
+ class ManagementInterface(Interface):
210
+ "Management Interface"
211
+ type: Literal["managementInterface"] = "managementInterface"
212
+
213
+ # Mgmt har inte vlan
214
+ vlan_id: Optional[int] = None
215
+
216
+ class RouteTarget(BaseModel):
217
+ value: str # TODO: Add constraints and validators...
218
+
219
+ class ImportExportPolicy(BaseModel):
220
+ export_route_target: Optional[List[RouteTarget]] = None
221
+ import_route_target: Optional[List[RouteTarget]] = None
222
+
223
+ class InterInstancePolicy(BaseModel):
224
+ import_export_policy: ImportExportPolicy
225
+
226
+ class NetworkInstance(BaseModel):
227
+ name: AttributeValue[str]
228
+ description: Optional[AttributeValue[str]] = None
229
+ vlans: Optional[Dict[str, Vlan]] = {}
230
+ interfaces: Optional[Dict[str, Reference]] = {}
231
+ inter_instance_policies: Optional[Dict[str, InterInstancePolicy]] = {}
232
+
233
+ class LacpConfig(BaseModel):
234
+ system_priority: Optional[AttributeValue[int]] = None
235
+ system_id_mac: Optional[AttributeValue[str]] = None
236
+ load_balance_algorithm: Optional[AttributeValue[list[Literal["src-mac", "dst-mac", "src-dst-mac", "src-ip", "dst-ip", "src-dst-ip", "src-port", "dst-port", "src-dst-port"]]]] = None
237
+
238
+ class Lacp(BaseModel):
239
+ config: Optional[LacpConfig] = LacpConfig()
240
+ interfaces: Optional[Dict[str, Interface]] = {}
241
+
242
+ # SNMP
243
+ class SnmpAccess(str, Enum):
244
+ READ_ONLY = "READ_ONLY"
245
+ READ_WRITE = "READ_WRITE"
246
+
247
+
248
+ class SnmpSecurityLevel(str, Enum):
249
+ NO_AUTH_NO_PRIV = "NO_AUTH_NO_PRIV"
250
+ AUTH_NO_PRIV = "AUTH_NO_PRIV"
251
+ AUTH_PRIV = "AUTH_PRIV"
252
+
253
+
254
+ class SnmpAuthProtocol(str, Enum):
255
+ MD5 = "MD5"
256
+ SHA1 = "SHA"
257
+ SHA224 = "SHA-224"
258
+ SHA256 = "SHA-256"
259
+ SHA384 = "SHA-384"
260
+ SHA512 = "SHA-512"
261
+
262
+
263
+ class SnmpPrivProtocol(str, Enum):
264
+ DES = "DES"
265
+ TRIPLE_DES = "3DES"
266
+ AES128 = "AES128"
267
+ AES192 = "AES192"
268
+ AES256 = "AES256"
269
+
270
+
271
+ class SnmpConfig(BaseModel):
272
+ enabled: AttributeValue[bool] = AttributeValue(value=False)
273
+ engine_id: Optional[AttributeValue[str]] = None
274
+ location: Optional[AttributeValue[str]] = None
275
+ contact: Optional[AttributeValue[str]] = None
276
+
277
+
278
+ class SnmpCommunity(BaseModel):
279
+ name: AttributeValue[str]
280
+ community: Optional[AttributeValue[str]] = None # Community string
281
+ access: Optional[AttributeValue[SnmpAccess]] = AttributeValue(value=SnmpAccess.READ_ONLY)
282
+ view: Optional[AttributeValue[str]] = None
283
+ ipv4_acl: Optional[AttributeValue[str]] = None # Cisco and "liknande" vendors
284
+ ipv6_acl: Optional[AttributeValue[str]] = None
285
+ source_interface: Optional[Reference] = None
286
+ clients: Optional[AttributeValue[List[str]]] = None # Juniper specific
287
+
288
+
289
+ class SnmpUser(BaseModel):
290
+ username: AttributeValue[str]
291
+ security_level: Optional[AttributeValue[SnmpSecurityLevel]] = AttributeValue(value=SnmpSecurityLevel.NO_AUTH_NO_PRIV)
292
+ auth_protocol: Optional[AttributeValue[SnmpAuthProtocol]] = None
293
+ auth_password: Optional[AttributeValue[str]] = None
294
+ priv_protocol: Optional[AttributeValue[SnmpPrivProtocol]] = None
295
+ priv_password: Optional[AttributeValue[str]] = None
296
+
297
+
298
+ class SnmpView(BaseModel):
299
+ name: AttributeValue[str]
300
+ oid: AttributeValue[str]
301
+ included: Optional[AttributeValue[bool]] = AttributeValue(value=True)
302
+
303
+
304
+ class SnmpServer(BaseModel):
305
+ name: str
306
+ address: AttributeValue[str]
307
+ port: Optional[AttributeValue[int]] = AttributeValue(value=162)
308
+ enabled: Optional[AttributeValue[bool]] = AttributeValue(value=True)
309
+ version: Optional[AttributeValue[Literal["v2c", "v3"]]] = None
310
+ community: Optional[AttributeValue[str]] = None
311
+ username: Optional[AttributeValue[str]] = None
312
+ security_level: Optional[AttributeValue[SnmpSecurityLevel]] = None
313
+ source_interface: Optional[Reference] = None
314
+ network_instance: Optional[AttributeValue[str]] = None
315
+
316
+ # ----------------------------
317
+ # Enum-based trap groups
318
+ # ----------------------------
319
+ # En stor class med Enum
320
+ class TrapEventOptions(str, Enum):
321
+ VRF_UP = "vrf-up"
322
+ VRF_DOWN = "vrf-down"
323
+ VNET_TRUNK_UP = "vnet-trunk-up"
324
+ VNET_TRUNK_DOWN = "vnet-trunk-down"
325
+ ALL = "all"
326
+ RfEvent = 'enabled'
327
+ VlanMembershipEvent = 'enabled'
328
+ ErrdisableEvent = 'enabled'
329
+ CHANGE = "change"
330
+ MOVE = "move"
331
+ THRESHOLD = "threshold"
332
+ AUTHENTICATION = "authentication"
333
+ LINKDOWN = "linkdown"
334
+ LINKUP = "linkup"
335
+ COLDSTART = "coldstart"
336
+ WARMSTART = "warmstart"
337
+ FLOWMONEVENT = "trapflowmonevent"
338
+ ENTITYPERFEVENT = "trapentityperfevent"
339
+ PCALLHOMEEVENT_MESSAGE_SEND_FAIL = "trapcallhomeevent_MESSAGE_SEND_FAIL"
340
+ CALLHOMEEVENT_SERVER_FAIL = "trapcallhomeevent_SERVER_FAIL"
341
+ TTYEVENT = "trapttyevent"
342
+ EIGRPEVENT = "trapeigrpevent"
343
+ OSPF_STATE_CHANGE = "ospf_state_change"
344
+ OSPF_ERRORS = "ospf_errors"
345
+ OSPF_RETRANSMIT = "ospf_retransmit"
346
+ OSPF_LSA = "ospf_lsa"
347
+ OSPF_CISCO_TRANS_CHANGE = "ospf_cisco_trans_change"
348
+ OSPF_CISCO_SHAMLINK_INTERFACE = "ospf_cisco_shamlink_interface"
349
+ OSPF_CISCO_SHAMLINK_NEIGHBOR = "ospf_cisco_shamlink_neighbor"
350
+ BFD_EVENT = "bfd_event"
351
+ CISCO_SMART_LICENSE_EVENT = "cisco_smart_license_event"
352
+ AUTH_FRAMEWORK_SEC_VIOLATION = "auth_framework_sec_violation"
353
+ REP_EVENT = "rep_event"
354
+ MEMORY_BUFFERPEAK = "memory_bufferpeak"
355
+ CONFIG_COPY = "config_copy"
356
+ CONFIG = "config"
357
+ CONFIG_CTID = "config_ctid"
358
+ ENERGYWISE_EVENT = "energy_wise_event"
359
+ FRU_CTRL_EVENT = "fru_ctrl_event"
360
+ ENTITY_EVENT = "entity_event"
361
+ FLASH_INSERTION = "flash_insertion"
362
+ FLASH_REMOVAL = "flash_removal"
363
+ FLASH_LOWSPACE = "flash_lowspace"
364
+ POWER_ETHERNET_POLICE = "power_ethernet_police"
365
+ POWER_ETHERNET_GROUP_THRESHOLD = "power_ethernet_group_threshold"
366
+ CPU_THRESHOLD = "cpu_threshold"
367
+ SYSLOG = "syslog"
368
+ UDLD_LINK_FAIL_RPT = "udld_link_fail_rpt"
369
+ UDLD_STATUS_CHANGE = "udld_status_change"
370
+ VTP_EVENT = "vtp_event"
371
+ VLAN_CREATE = "vlancreate"
372
+ VLAN_DELETE = "vlandelete"
373
+ PORT_SECURITY = "port_security"
374
+ ENV_MON = "env_mon"
375
+ STACKWISE = "stackwise"
376
+ MVPN = "mvpn"
377
+ PW_VC = "pw_vc"
378
+ IPSLA = "ipsla"
379
+ DHCP = "dhcp"
380
+ EVENT_MANAGER = "event_manager"
381
+ IKE_POLICY_ADD = "ike_policy_add"
382
+ IKE_POLICY_DELETE = "ike_policy_delete"
383
+ IKE_TUNNEL_START = "ike_tunnel_start"
384
+ IKE_TUNNEL_STOP = "ike_tunnel_stop"
385
+ IPSEC_CRYPTOMAP_ADD = "ipsec_cryptomap_add"
386
+ IPSEC_CRYPTOMAP_DELETE = "ipsec_cryptomap_delete"
387
+ IPSEC_CRYPTOMAP_ATTACH = "ipsec_cryptomap_attach"
388
+ IPSEC_CRYPTOMAP_DETACH = "ipsec_cryptomap_detach"
389
+ IPSEC_TUNNEL_START = "ipsec_tunnel_start"
390
+ IPSEC_TUNNEL_STOP = "ipsec_tunnel_stop"
391
+ IPSEC_TOO_MANY_SAS = "ipsec_too_many_sas"
392
+ OSPFV3_STATE_CHANGE = "ospfv3_state_change"
393
+ OSPFV3_ERRORS = "ospfv3_errors"
394
+ IP_MULTICAST = "ip_multicast"
395
+ MSDP = "msdp"
396
+ PIM_NEIGHBOR_CHANGE = "pim_neighbor_change"
397
+ PIM_RP_MAPPING_CHANGE = "pim_rp_mapping_change"
398
+ INVALID_PIM_MESSAGE = "invalid_pim_message"
399
+ BRIDGE_NEWROOT = "bridge_newroot"
400
+ BRIDGE_TOPOLOGYCHANGE = "bridge_topologychange"
401
+ STPX_INCONSISTENCY = "stpx_inconsistency"
402
+ STPX_ROOT_INCONSISTENCY = "stpx_root_inconsistency"
403
+ STPX_LOOP_INCONSISTENCY = "stpx_loop_inconsistency"
404
+ BGP_CBG2 = "bgp_cbg2"
405
+ HSRP = "hsrp"
406
+ ISIS = "isis"
407
+ CEF_RESOURCE_FAILURE = "cef_resource_failure"
408
+ CEF_PEER_STATE_CHANGE = "cef_peer_state_change"
409
+ CEF_PEER_FIB_STATE_CHANGE = "cef_peer_fib_state_change"
410
+ CEF_INCONSISTENCY = "cef_inconsistency"
411
+ LISP = "lisp"
412
+ NHRP_NHS = "nhrp_nhs"
413
+ NHRP_NHC = "nhrp_nhc"
414
+ NHRP_NHP = "nhrp_nhp"
415
+ NHRP_QUOTA_EXCEEDED = "nhrp_quota_exceeded"
416
+ LOCAL_AUTH = "local_auth"
417
+ ENTITY_DIAG_BOOT_UP_FAIL = "entity_diag_boot_up_fail"
418
+ ENTITY_DIAG_HM_TEST_RECOVER = "entity_diag_hm_test_recover"
419
+ ENTITY_DIAG_HM_THRESH_REACHED = "entity_diag_hm_thresh_reached"
420
+ ENTITY_DIAG_SCHEDULED_TEST_FAIL = "entity_diag_scheduled_test_fail"
421
+ MPLS_RFC_LDP = "mpls_rfc_ldp"
422
+ MPLS_LDP = "mpls_ldp"
423
+ MPLS_RFC_TRAFFIC_ENG = "mpls_rfc_traffic_eng"
424
+ MPLS_TRAFFIC_ENG = "mpls_traffic_eng"
425
+ MPLS_FAST_REROUTE_PROTECTED = "mpls_fast_reroute_protected"
426
+ MPLS_VPN = "mpls_vpn"
427
+ MPLS_RFC_VPN = "mpls_rfc_vpn"
428
+ BULKSTAT_COLLECTION = "bulkstat_collection"
429
+ BULKSTAT_TRANSFER = "bulkstat_transfer"
430
+
431
+ class TrapEvent(BaseModel):
432
+ name: str
433
+ event_name: TrapEventOptions
434
+
435
+ #class SnmpTrap(BaseModel): ...
436
+
437
+ class Snmp(BaseModel):
438
+ config: SnmpConfig = SnmpConfig()
439
+ communities: Optional[Dict[str, SnmpCommunity]] = {}
440
+ users: Optional[Dict[str, SnmpUser]] = {}
441
+ trap_servers: Optional[Dict[str, SnmpServer]] = {}
442
+ trap_events: Optional[Dict[str, TrapEvent]] = {}
443
+ views: Optional[Dict[str, SnmpView]] = {}
444
+
445
+ # AAA
446
+ class aaaBaseClass(BaseModel):
447
+ name: str = None
448
+
449
+ class aaaTacacsAttributes(aaaBaseClass):
450
+ port: Optional[int] = 49
451
+ secret_key: Optional[str] = None
452
+ secret_key_hashed: Optional[str] = None
453
+ address: Optional[str] = None
454
+ timeout: Optional[int] = 30
455
+ source_address: Optional[str] = None #Optional[Reference] = None # should be reference
456
+
457
+ class aaaRadiusAttributes(aaaBaseClass):
458
+ auth_port: Optional[int] = 1812
459
+ acct_port: Optional[int] = 1813
460
+ secret_key: Optional[str] = None
461
+ secret_key_hashed: Optional[str] = None
462
+ address: Optional[str] = None
463
+ timeout: Optional[int] = 30
464
+ source_address: Optional[str] = None #Optional[Reference] = None # should be reference
465
+ retransmit_attempts: Optional[int] = 3
466
+
467
+ class aaaServerGroupAttributes(BaseModel):
468
+ enable: Optional[bool] = False
469
+ type: Optional[Literal['tacacs','radius']] = None
470
+ #servers: Optional[list] = None
471
+ #address: Optional[str] = None
472
+ #timeout: Optional[int] = 30
473
+ tacacs: Optional[Reference] = None
474
+ radius: Optional[Reference] = None
475
+
476
+ # Authentication Models
477
+ class aaaAuthenticationMethods(aaaBaseClass):
478
+ method: Optional[List[str]] = None # Ex. ['TACACS_GROUP','LOCAL'], TACACS_GROUP is reference to server group
479
+
480
+ class authenticationUser(aaaBaseClass):
481
+ username: Optional[str] = None
482
+ password: Optional[str] = None
483
+ password_hahsed: Optional[str] = None
484
+ ssh_key: Optional[str] = None
485
+ role: Optional[str] = None
486
+
487
+ class aaaAuthenticationUsers(aaaBaseClass):
488
+ username: Optional[Dict[str, authenticationUser]] = {}
489
+
490
+ class adminUser(aaaBaseClass): # when to use this?
491
+ admin_password: Optional[str] = None
492
+ admin_password_hashed: Optional[str] = None
493
+
494
+ class aaaAuthenticationAdminUsers(BaseModel):
495
+ config: Optional[Dict[str, adminUser]] = {}
496
+
497
+ class aaaAuthentication(BaseModel):
498
+ config: Optional[Dict[str, aaaAuthenticationMethods]] = {}
499
+ admin_user: Optional[Dict[str, aaaAuthenticationAdminUsers]] = {}
500
+ users: Optional[Dict[str, aaaAuthenticationUsers]] = {}
501
+
502
+ # Authorization Models
503
+ class aaaAuthorizationMethods(aaaBaseClass):
504
+ method: Optional[List[str]] = None # Ex. ['TACACS_GROUP','LOCAL']
505
+
506
+ class aaaAuthorizationEvent(aaaBaseClass):
507
+ event_type: dict = {
508
+ 'event-type':'command',
509
+ 'method':['tacacs_group']
510
+ }
511
+
512
+ class aaaAuthorizationEvents(BaseModel):
513
+ event: Optional[Dict[str, aaaAuthorizationEvent]] = {}
514
+
515
+ class aaaAuthorization(BaseModel):
516
+ config: Optional[Dict[str, aaaAuthorizationMethods]] = {}
517
+ events: Optional[Dict[str, aaaAuthorizationEvents]] = {}
518
+
519
+ # Accounting Models
520
+ class aaaAccountingMethods(BaseModel):
521
+ method: Optional[List[str]] = None # Ex. ['TACACS_GROUP','LOCAL']
522
+
523
+ class aaaAccountingEvents(BaseModel):
524
+ event: list = [
525
+ {
526
+ 'event-type': 'command',
527
+ 'config': {
528
+ 'event-type': 'command',
529
+ 'method': ['tacacs_group']
530
+ }
531
+ },
532
+ {
533
+ 'event-type': 'system',
534
+ 'config': {
535
+ 'event-type': 'system',
536
+ 'method': ['tacacs_group']
537
+ }
538
+ }
539
+ ]
540
+
541
+ class aaaAccounting(BaseModel):
542
+ config: aaaAccountingMethods = aaaAccountingMethods()
543
+ events: aaaAccountingEvents = aaaAccountingEvents()
544
+
545
+ class TripleA(BaseModel):
546
+ #config: dict = None
547
+ server_groups: Optional[Dict[str, aaaServerGroupAttributes]] = {}
548
+ tacacs: Optional[Dict[str, aaaTacacsAttributes]] = {}
549
+ radius: Optional[Dict[str, aaaRadiusAttributes]] = {}
550
+ authentication: aaaAuthentication = aaaAuthentication()
551
+ authorization: aaaAuthorization = aaaAuthorization()
552
+ accounting: aaaAccounting = aaaAccounting()
553
+
554
+ class System(BaseModel):
555
+ config: SystemConfig = SystemConfig()
556
+ aaa: Optional[TripleA] = TripleA()
557
+ logging: Optional[LoggingComponents] = LoggingComponents() # Trying to avoid using "Logging" or "logging" as names for anything due to conflicts with standard lib.
558
+ ntp: Optional[Ntp] = Ntp()
559
+ ssh: Optional[Ssh] = Ssh()
560
+ snmp: Optional[Snmp] = {}
561
+
562
+ # For different types of interfaces that are fine for response model:
563
+ InterfaceType = Union[
564
+ EthernetCsmacdInterface,
565
+ Ieee8023adLagInterface,
566
+ L3IpvlanInterface,
567
+ SoftwareLoopbackInterface,
568
+ SubInterface,
569
+ ManagementInterface,
570
+ ]
571
+
572
+ class ComposedConfiguration(BaseModel):
573
+ system: Optional[System] = System()
574
+ acl: Optional[Acl] = Acl()
575
+ lldp: Optional[Lldp] = Lldp()
576
+ lacp: Optional[Lacp] = Lacp()
577
+ interfaces: Dict[str, InterfaceType] = {}
578
+ network_instances: Dict[str, NetworkInstance] = {"global": NetworkInstance(name="global")}
579
+ stp: Optional[SpanningTree] = {}
@@ -0,0 +1,42 @@
1
+ from pydantic import BaseModel, ConfigDict, field_validator, field_serializer, PrivateAttr
2
+ from sqlmodel import SQLModel, Field
3
+ from typing import Literal, Callable, Optional, Any
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+
7
+
8
+ class EVType(Enum):
9
+ data = "data"
10
+ resource = "resource"
11
+
12
+ class ExternalValue(SQLModel, table=True):
13
+ model_config = ConfigDict(arbitrary_types_allowed=True)
14
+ attr_ptr: str = Field(default=None, primary_key=True)
15
+
16
+ # query: dict # same query as was used for fetching the data
17
+ query: str = '{"json_query": "in_stringformat"}'
18
+ value: Optional[str] = None
19
+ kind: str # object kind/type
20
+ ev_type: EVType = Field(default=EVType.data)
21
+ plugin: str
22
+ resolved: bool = Field(default=False) # True when value has been resolved
23
+ resolved_at: Optional[datetime] = Field(default=None) # Only set when resolved
24
+
25
+ # Privat attribut för callable (inte i JSON eller databas)
26
+ _callable: Optional[Callable] = PrivateAttr(default=None)
27
+
28
+ @field_validator('ev_type', mode='before')
29
+ @classmethod
30
+ def validate_ev_type(cls, v):
31
+ if isinstance(v, str):
32
+ return EVType(v)
33
+ return v
34
+
35
+ @field_serializer('ev_type')
36
+ def serialize_ev_type(self, value) -> str:
37
+ if isinstance(value, EVType):
38
+ return value.value
39
+ if isinstance(value, str):
40
+ return value
41
+ return str(value)
42
+
@@ -0,0 +1,77 @@
1
+ from pydantic import BaseModel
2
+ from acex_devkit.models.attribute_value import AttributeValue
3
+ from enum import Enum
4
+ from typing import Optional, Dict
5
+
6
+
7
+ class LoggingServerBase(BaseModel): ...
8
+ #name: str = None
9
+
10
+
11
+ class LoggingSeverity(str, Enum):
12
+ EMERGENCY = "EMERGENCY"
13
+ ALERT = "ALERT"
14
+ CRITICAL = "CRITICAL"
15
+ ERROR = "ERROR"
16
+ WARNING = "WARNING"
17
+ NOTICE = "NOTICE"
18
+ INFORMATIONAL = "INFORMATIONAL"
19
+ DEBUG = "DEBUG"
20
+
21
+ class LoggingFacility(str, Enum):
22
+ # Some are specific for Juniper devices and are taken directly from their documentation.
23
+ KERN = "KERN"
24
+ USER = "USER"
25
+ DAEMON = "DAEMON"
26
+ AUTHORIZATION = "AUTHORIZATION"
27
+ FTP = "FTP"
28
+ NTP = "NTP"
29
+ DFC = "DFC"
30
+ EXTERNAL = "EXTERNAL"
31
+ FIREWALL = "FIREWALL"
32
+ PFE = "PFE"
33
+ CONFLICTLOG = "CONFLICTLOG"
34
+ CHANGELOG = "CHANGELOG"
35
+ INTERACTIVE_COMMANDS = "INTERACTIVE_COMMANDS"
36
+
37
+ class Reference(BaseModel): ...
38
+
39
+ class LoggingConfig(BaseModel):
40
+ rate_limit: Optional[AttributeValue[int]] = None
41
+ severity: Optional[AttributeValue[LoggingSeverity]] = None
42
+ buffer_size: Optional[AttributeValue[int]] = None
43
+
44
+ class Console(BaseModel):
45
+ name: str = None
46
+ line_number: int = None
47
+ logging_synchronous: bool = True
48
+
49
+ class RemoteServer(BaseModel):
50
+ name: str = None
51
+ host: str = None
52
+ port: Optional[int] = 514
53
+ transfer: Optional[str] = 'udp'
54
+ source_address: Optional[AttributeValue[str]] = None # Can be an IP address or an interface reference
55
+
56
+ class VtyLine(BaseModel):
57
+ name: str = None
58
+ line_number: int = None
59
+ logging_synchronous: bool = True
60
+ transport_input: Optional[str] = 'ssh' # default is SSH. Mostly used by Cisco.
61
+
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
70
+
71
+ class LoggingEvent(BaseModel):
72
+ enabled: bool
73
+ severity: LoggingSeverity
74
+
75
+
76
+ class LoggingEvents(BaseModel):
77
+ events: Optional[Dict[str, LoggingEvent]] = {}
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Ned(BaseModel):
5
+ name: str
6
+ package_name: str
7
+ version: str
8
+ description: str
9
+ filename: str
@@ -0,0 +1,22 @@
1
+
2
+
3
+ from acex_devkit.models.composed_configuration import ComposedConfiguration
4
+
5
+
6
+ class LogicalNodeResponse(BaseModel):
7
+ hostname: Optional[str] = Field('R1', title='Hostname')
8
+ role: Optional[str] = Field('core', title='Role')
9
+ site: Optional[str] = Field('HQ', title='Site')
10
+ sequence: Optional[int] = Field(1, title='Sequence')
11
+ id: Optional[int] = Field(None, title='Id')
12
+ configuration: Optional[ComposedConfiguration] = ComposedConfiguration()
13
+ meta_data: Optional[Dict[str, Any]] = Field(None, title='Meta Data')
14
+
15
+ class NodeResponse(BaseModel):
16
+ asset_ref_id: int = Field(..., title='Asset Ref Id')
17
+ asset_ref_type: Optional[AssetRefType] = 'asset'
18
+ logical_node_id: int = Field(..., title='Logical Node Id')
19
+ asset: AssetResponse
20
+ logical_node: LogicalNodeResponse
21
+
22
+
@@ -0,0 +1,64 @@
1
+ from pydantic import BaseModel
2
+ from acex_devkit.models.attribute_value import AttributeValue
3
+ from enum import Enum
4
+ from typing import Optional, Dict
5
+
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] = False # Disabled by default
9
+ bpdu_guard: Optional[bool] = False # Disabled by default
10
+ loop_guard: Optional[bool] = False # Disabled by default
11
+ portfast: Optional[bool] = False # Disabled by default. Global setting for access ports.
12
+ bridge_assurance: Optional[bool] = False # Disabled by default. Only supported by MST and PVRST+
13
+ #interfaces: Optional[Dict[str, Reference]] = {}
14
+
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
21
+
22
+ ## RSTP
23
+ class RstpAttributes(SpanningTreeModeConfig): ...
24
+
25
+ class RSTPConfig(BaseModel):
26
+ config: RstpAttributes = RstpAttributes()
27
+
28
+ ### MSTP
29
+ class MstpInstanceAttributes(SpanningTreeModeConfig):
30
+ instance_id: AttributeValue[int] # range: 1..4094
31
+ name: Optional[AttributeValue[str]] = None
32
+ vlan: Optional[AttributeValue[list[int]]] = None # List of VLANs mapped to the MST instance
33
+
34
+ class MstpAttributes(SpanningTreeModeConfig):
35
+ revision: Optional[AttributeValue[int]] = None
36
+ max_hop: Optional[AttributeValue[int]] = None # Range 1..255
37
+
38
+ class MSTPConfig(BaseModel):
39
+ config: MstpAttributes = MstpAttributes()
40
+ mst_instances: Optional[Dict[str, MstpInstanceAttributes]] = {}
41
+
42
+ ### Rapid PVST
43
+ class RapidPVSTAttributes(SpanningTreeModeConfig):
44
+ """
45
+ Docstring for RapidPVSTAttributes
46
+ vlan can be a string or list. Depending on how NED is built it will check wether it's a single VLAN or multiple VLANs and then format the data to the correct format
47
+ for the command of the specific vendor.
48
+ For example for Cisco:
49
+ * Single VLAN
50
+ spanning-tree vlan 10 priority 8192
51
+ * Multiple VLANs
52
+ spanning-tree vlan 10-30 priority 8192
53
+ """
54
+ #vlan_id: Optional[AttributeValue[int]] = None # Single VLAN ID or list of VLANs using Rapid PVST+
55
+ vlan: Optional[AttributeValue[int | list[int]]] = None # Single VLAN ID or list of VLANs using Rapid PVST+
56
+
57
+ class RapidPVSTConfig(BaseModel):
58
+ vlan: Optional[Dict[str, RapidPVSTAttributes]] = {}
59
+
60
+ class SpanningTree(BaseModel):
61
+ config: SpanningTreeGlobalAttributes = SpanningTreeGlobalAttributes()
62
+ rstp: RSTPConfig = RSTPConfig()
63
+ mstp: MSTPConfig = MSTPConfig()
64
+ rapidpvst: RapidPVSTConfig = RapidPVSTConfig()
File without changes