annet 0.16.8__py3-none-any.whl → 0.16.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of annet might be problematic. Click here for more details.

Files changed (34) hide show
  1. annet/adapters/fetchers/__init__.py +0 -0
  2. annet/adapters/fetchers/stub/__init__.py +0 -0
  3. annet/adapters/fetchers/stub/fetcher.py +19 -0
  4. annet/adapters/file/__init__.py +0 -0
  5. annet/adapters/file/provider.py +226 -0
  6. annet/adapters/netbox/common/models.py +108 -3
  7. annet/adapters/netbox/v37/storage.py +31 -3
  8. annet/annlib/netdev/views/hardware.py +31 -0
  9. annet/bgp_models.py +266 -0
  10. annet/configs/context.yml +2 -3
  11. annet/configs/logging.yaml +1 -0
  12. annet/generators/__init__.py +18 -4
  13. annet/mesh/__init__.py +16 -0
  14. annet/mesh/basemodel.py +180 -0
  15. annet/mesh/device_models.py +62 -0
  16. annet/mesh/executor.py +248 -0
  17. annet/mesh/match_args.py +165 -0
  18. annet/mesh/models_converter.py +84 -0
  19. annet/mesh/peer_models.py +98 -0
  20. annet/mesh/registry.py +212 -0
  21. annet/rulebook/routeros/__init__.py +0 -0
  22. annet/rulebook/routeros/file.py +5 -0
  23. annet/storage.py +41 -2
  24. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/METADATA +3 -1
  25. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/RECORD +34 -15
  26. annet_generators/example/__init__.py +1 -3
  27. annet_generators/mesh_example/__init__.py +9 -0
  28. annet_generators/mesh_example/bgp.py +43 -0
  29. annet_generators/mesh_example/mesh_logic.py +28 -0
  30. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/AUTHORS +0 -0
  31. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/LICENSE +0 -0
  32. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/WHEEL +0 -0
  33. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/entry_points.txt +0 -0
  34. {annet-0.16.8.dist-info → annet-0.16.9.dist-info}/top_level.txt +0 -0
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ from annet.deploy import Fetcher
2
+ from annet.connectors import AdapterWithConfig
3
+ from typing import Dict, List, Any
4
+ from annet.storage import Device
5
+
6
+
7
+ class StubFetcher(Fetcher, AdapterWithConfig):
8
+ @classmethod
9
+ def with_config(cls, **kwargs: Dict[str, Any]) -> Fetcher:
10
+ return cls(**kwargs)
11
+
12
+ def fetch_packages(self, devices: List[Device],
13
+ processes: int = 1, max_slots: int = 0):
14
+ raise NotImplementedError()
15
+
16
+ def fetch(self, devices: List[Device],
17
+ files_to_download: Dict[str, List[str]] = None,
18
+ processes: int = 1, max_slots: int = 0):
19
+ raise NotImplementedError()
File without changes
@@ -0,0 +1,226 @@
1
+ from annet.annlib.netdev.views.dump import DumpableView
2
+ from annet.storage import Query
3
+ from dataclasses import dataclass, fields
4
+ from typing import List, Iterable, Optional, Any
5
+ from annet.storage import StorageProvider, Storage
6
+ from annet.connectors import AdapterWithName
7
+ from annet.storage import Device as DeviceCls
8
+ from annet.annlib.netdev.views.hardware import vendor_to_hw, HardwareView
9
+ import yaml
10
+
11
+
12
+ @dataclass
13
+ class Interface(DumpableView):
14
+ name: str
15
+ description: str
16
+ enabled: bool = True
17
+
18
+ @property
19
+ def _dump__list_key(self):
20
+ return self.name
21
+
22
+
23
+ @dataclass
24
+ class DeviceStorage:
25
+ fqdn: str
26
+ vendor: str
27
+ hostname: Optional[str] = None
28
+ serial: Optional[str] = None
29
+ id: Optional[str] = None
30
+ interfaces: Optional[list[Interface]] = None
31
+ storage: Optional[Storage] = None
32
+
33
+ def __post_init__(self):
34
+ if not self.id:
35
+ self.id = self.fqdn
36
+ if not self.hostname:
37
+ self.hostname = self.fqdn.split(".")[0]
38
+ hw = vendor_to_hw(self.vendor)
39
+ if not hw:
40
+ raise Exception("unknown vendor")
41
+ self.hw = hw
42
+ if isinstance(self.interfaces, list):
43
+ interfaces = []
44
+ for iface in self.interfaces:
45
+ try:
46
+ interfaces.append(Interface(**iface))
47
+ except Exception as e:
48
+ raise Exception("unable to parse %s as Interface %s" % (iface, e))
49
+ self.interfaces = interfaces
50
+
51
+ def set_storage(self, storage: Storage):
52
+ self.storage = storage
53
+
54
+
55
+ @dataclass
56
+ class Device(DeviceCls, DumpableView):
57
+ dev: DeviceStorage
58
+
59
+ @property
60
+ def hostname(self) -> str:
61
+ return self.dev.hostname
62
+
63
+ @property
64
+ def fqdn(self) -> str:
65
+ return self.dev.fqdn
66
+
67
+ @property
68
+ def id(self):
69
+ return self.dev.id
70
+
71
+ def __hash__(self):
72
+ return hash((self.id, type(self)))
73
+
74
+ def __eq__(self, other):
75
+ return type(self) is type(other) and self.fqdn == other.fqdn and self.vendor == other.vendor
76
+
77
+ def is_pc(self) -> bool:
78
+ return False
79
+
80
+ @property
81
+ def storage(self) -> Storage:
82
+ return self
83
+
84
+ @property
85
+ def hw(self) -> HardwareView:
86
+ return self.dev.hw
87
+
88
+ @property
89
+ def breed(self) -> str:
90
+ return self.dev.hw.vendor
91
+
92
+ @property
93
+ def neighbours_ids(self):
94
+ pass
95
+
96
+
97
+ @dataclass
98
+ class Devices:
99
+ devices: list[Device]
100
+
101
+ def __post_init__(self):
102
+ if isinstance(self.devices, list):
103
+ devices = []
104
+ for dev in self.devices:
105
+ try:
106
+ devices.append(Device(dev=DeviceStorage(**dev)))
107
+ except Exception as e:
108
+ raise Exception("unable to parse %s as Device %s" % (dev, e))
109
+ self.devices = devices
110
+
111
+
112
+ class Provider(StorageProvider, AdapterWithName):
113
+ def storage(self):
114
+ return storage_factory
115
+
116
+ def opts(self):
117
+ return StorageOpts
118
+
119
+ def query(self):
120
+ return Query
121
+
122
+ @classmethod
123
+ def name(cls) -> str:
124
+ return "file"
125
+
126
+
127
+ @dataclass
128
+ class Query(Query):
129
+ query: List[str]
130
+
131
+ @classmethod
132
+ def new(cls, query: str | Iterable[str], hosts_range: Optional[slice] = None) -> "Query":
133
+ if hosts_range is not None:
134
+ raise ValueError("host_range is not supported")
135
+ return cls(query=list(query))
136
+
137
+ @property
138
+ def globs(self):
139
+ return self.query
140
+
141
+ def is_empty(self) -> bool:
142
+ return len(self.query) == 0
143
+
144
+
145
+ class StorageOpts:
146
+ def __init__(self, path: str):
147
+ self.path = path
148
+
149
+ @classmethod
150
+ def parse_params(cls, conf_params: Optional[dict[str, str]], cli_opts: Any):
151
+ path = conf_params.get("path")
152
+ if not path:
153
+ raise Exception("empty path")
154
+ return cls(path=path)
155
+
156
+
157
+ def storage_factory(opts: StorageOpts) -> Storage:
158
+ return FS(opts)
159
+
160
+
161
+ class FS(Storage):
162
+ def __init__(self, opts: StorageOpts):
163
+ self.opts = opts
164
+ self.inventory: Devices = read_inventory(opts.path, self)
165
+
166
+ def __enter__(self):
167
+ return self
168
+
169
+ def __exit__(self, _, __, ___):
170
+ pass
171
+
172
+ def resolve_object_ids_by_query(self, query: Query) -> list[str]:
173
+ result = filter_query(self.inventory.devices, query)
174
+ return [dev.fqdn for dev in result]
175
+
176
+ def resolve_fdnds_by_query(self, query: Query) -> list[str]:
177
+ result = filter_query(self.inventory.devices, query)
178
+ return [dev.fqdn for dev in result]
179
+
180
+ def make_devices(
181
+ self,
182
+ query: Query | list,
183
+ preload_neighbors=False,
184
+ use_mesh=None,
185
+ preload_extra_fields=False,
186
+ **kwargs,
187
+ ) -> list[Device]:
188
+ if isinstance(query, list):
189
+ query = Query.new(query)
190
+ result = filter_query(self.inventory.devices, query)
191
+ return result
192
+
193
+ def get_device(self, obj_id: str, preload_neighbors=False, use_mesh=None, **kwargs) -> Device:
194
+ result = filter_query(self.inventory.devices, Query.new(obj_id))
195
+ if not result:
196
+ raise Exception("not found")
197
+ return result[0]
198
+
199
+ def flush_perf(self):
200
+ pass
201
+
202
+
203
+ def filter_query(devices: list[Device], query: Query) -> list[Device]:
204
+ result: list[Device] = []
205
+ for dev in devices:
206
+ if dev.fqdn in query.query:
207
+ result.append(dev)
208
+ return result
209
+
210
+
211
+ def read_inventory(path: str, storage: Storage) -> Devices:
212
+ with open(path, "r") as f:
213
+ data = f.read()
214
+ file_data = yaml.load(data, Loader=yaml.BaseLoader)
215
+ res = dataclass_from_dict(Devices, file_data)
216
+ for dev in res.devices:
217
+ dev.dev.set_storage(storage)
218
+ return res
219
+
220
+
221
+ def dataclass_from_dict(klass: type, d: dict[str, Any]):
222
+ try:
223
+ fieldtypes = {f.name: f.type for f in fields(klass)}
224
+ except TypeError:
225
+ return d
226
+ return klass(**{f: dataclass_from_dict(fieldtypes[f], d[f]) for f in d})
@@ -1,9 +1,10 @@
1
1
  from dataclasses import dataclass, field
2
- from datetime import datetime
3
- from typing import List, Optional, Any, Dict
2
+ from datetime import datetime, timezone
3
+ from ipaddress import ip_interface, IPv6Interface
4
+ from typing import List, Optional, Any, Dict, Sequence, Callable
4
5
 
5
6
  from annet.annlib.netdev.views.dump import DumpableView
6
- from annet.annlib.netdev.views.hardware import HardwareView
7
+ from annet.annlib.netdev.views.hardware import HardwareView, lag_name, svi_name
7
8
  from annet.storage import Storage
8
9
 
9
10
 
@@ -124,6 +125,33 @@ class Interface(Entity):
124
125
  ip_addresses: List[IpAddress] = field(default_factory=list)
125
126
  vrf: Optional[Entity] = None
126
127
  mtu: int | None = None
128
+ lag: Entity | None = None
129
+ lag_min_links: int | None = None
130
+
131
+ def add_addr(self, address_mask: str, vrf: str | None) -> None:
132
+ addr = ip_interface(address_mask)
133
+ if vrf is None:
134
+ vrf_obj = None
135
+ else:
136
+ vrf_obj = Entity(id=0, name=vrf)
137
+
138
+ if isinstance(addr, IPv6Interface):
139
+ family = IpFamily(value=6, label="IPv6")
140
+ else:
141
+ family = IpFamily(value=4, label="IPv4")
142
+ self.ip_addresses.append(IpAddress(
143
+ id=0,
144
+ display=address_mask,
145
+ address=address_mask,
146
+ vrf=vrf_obj,
147
+ prefix=None,
148
+ family=family,
149
+ created=datetime.now(timezone.utc),
150
+ last_updated=datetime.now(timezone.utc),
151
+ tags=[],
152
+ status=Label(value="active", label="Active"),
153
+ assigned_object_id=self.id,
154
+ ))
127
155
 
128
156
 
129
157
  @dataclass
@@ -160,6 +188,17 @@ class NetboxDevice(Entity):
160
188
  neighbours: Optional[List["NetboxDevice"]]
161
189
 
162
190
  # compat
191
+ @property
192
+ def neighbours_fqdns(self) -> list[str]:
193
+ if not self.neighbours:
194
+ return []
195
+ return [dev.fqdn for dev in self.neighbours]
196
+
197
+ @property
198
+ def neighbours_ids(self):
199
+ if not self.neighbours:
200
+ return []
201
+ return [dev.id for dev in self.neighbours]
163
202
 
164
203
  def __hash__(self):
165
204
  return hash((self.id, type(self)))
@@ -169,3 +208,69 @@ class NetboxDevice(Entity):
169
208
 
170
209
  def is_pc(self) -> bool:
171
210
  return self.device_type.manufacturer.name == "Mellanox"
211
+
212
+ def _make_interface(self, name: str, type: InterfaceType) -> Interface:
213
+ return Interface(
214
+ name=name,
215
+ device=self,
216
+ enabled=True,
217
+ description="",
218
+ type=type,
219
+ id=0,
220
+ vrf=None,
221
+ display=name,
222
+ untagged_vlan=None,
223
+ tagged_vlans=[],
224
+ ip_addresses=[],
225
+ connected_endpoints=[],
226
+ mode=None,
227
+ )
228
+
229
+ def _lag_name(self, lag: int) -> str:
230
+ return lag_name(self.hw, lag)
231
+
232
+ def make_lag(self, lag: int, ports: Sequence[str], lag_min_links: int | None) -> Interface:
233
+ new_name = self._lag_name(lag)
234
+ for target_interface in self.interfaces:
235
+ if target_interface.name == new_name:
236
+ return target_interface
237
+ lag_interface = self._make_interface(
238
+ name=new_name,
239
+ type=InterfaceType(value="lag", label="Link Aggregation Group (LAG)"),
240
+ )
241
+ lag_interface.lag_min_links = lag_min_links
242
+ for interface in self.interfaces:
243
+ if interface.name in ports:
244
+ interface.lag = lag_interface
245
+ self.interfaces.append(lag_interface)
246
+ return lag_interface
247
+
248
+ def _svi_name(self, svi: int) -> str:
249
+ return svi_name(self.hw, svi)
250
+
251
+ def add_svi(self, svi: int) -> Interface:
252
+ name = self._svi_name(svi)
253
+ for interface in self.interfaces:
254
+ if interface.name == name:
255
+ return interface
256
+ interface = self._make_interface(
257
+ name=name,
258
+ type=InterfaceType("virtual", "Virtual")
259
+ )
260
+ self.interfaces.append(interface)
261
+ return interface
262
+
263
+ def _subif_name(self, interface: str, subif: int) -> str:
264
+ return f"{interface}.{subif}"
265
+
266
+ def add_subif(self, interface: str, subif: int) -> Interface:
267
+ name = self._subif_name(interface, subif)
268
+ for target_port in self.interfaces:
269
+ if target_port.name == name:
270
+ return target_port
271
+ target_port = self._make_interface(
272
+ name=name,
273
+ type=InterfaceType("virtual", "Virtual")
274
+ )
275
+ self.interfaces.append(target_port)
276
+ return target_port
@@ -15,8 +15,7 @@ from annet.adapters.netbox.common.manufacturer import (
15
15
  from annet.adapters.netbox.common.query import NetboxQuery
16
16
  from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
17
17
  from annet.annlib.netdev.views.hardware import HardwareView
18
- from annet.storage import Storage
19
-
18
+ from annet.storage import Storage, Device, Interface
20
19
 
21
20
  logger = getLogger(__name__)
22
21
 
@@ -68,7 +67,9 @@ def extend_device(
68
67
  return res
69
68
 
70
69
 
71
- @impl_converter
70
+ @impl_converter(
71
+ recipe=[link_constant(P[models.Interface].lag_min_links, value=None)],
72
+ )
72
73
  def extend_interface(
73
74
  interface: api_models.Interface,
74
75
  ip_addresses: List[models.IpAddress],
@@ -89,6 +90,7 @@ class NetboxStorageV37(Storage):
89
90
  url=opts.url,
90
91
  token=opts.token,
91
92
  )
93
+ self._all_fqdns: Optional[list[str]] = None
92
94
 
93
95
  def __enter__(self):
94
96
  return self
@@ -106,6 +108,14 @@ class NetboxStorageV37(Storage):
106
108
  d.name for d in self._load_devices(query)
107
109
  ]
108
110
 
111
+ def resolve_all_fdnds(self) -> list[str]:
112
+ if self._all_fqdns is None:
113
+ self._all_fqdns = [
114
+ d.name
115
+ for d in self.netbox.dcim_all_devices().results
116
+ ]
117
+ return self._all_fqdns
118
+
109
119
  def make_devices(
110
120
  self,
111
121
  query: Union[NetboxQuery, list],
@@ -218,6 +228,24 @@ class NetboxStorageV37(Storage):
218
228
  def flush_perf(self):
219
229
  pass
220
230
 
231
+ def search_connections(self, device: Device, neighbor: Device) -> list[tuple[Interface, Interface]]:
232
+ if device.storage is not self:
233
+ raise ValueError("device does not belong to this storage")
234
+ if neighbor.storage is not self:
235
+ raise ValueError("neighbor does not belong to this storage")
236
+ # both devices are NetboxDevice if they are loaded from this storage
237
+ res = []
238
+ for local_port in device.interfaces:
239
+ if not local_port.connected_endpoints:
240
+ continue
241
+ for endpoint in local_port.connected_endpoints:
242
+ if endpoint.device.id == neighbor.id:
243
+ for remote_port in neighbor.interfaces:
244
+ if remote_port.name == endpoint.name:
245
+ res.append((local_port, remote_port))
246
+ break
247
+ return res
248
+
221
249
 
222
250
  def _match_query(query: NetboxQuery, device_data: api_models.Device) -> bool:
223
251
  for subquery in query.globs:
@@ -123,3 +123,34 @@ def vendor_to_hw(vendor):
123
123
  None,
124
124
  )
125
125
  return hw
126
+
127
+
128
+ def lag_name(hw: HardwareView, nlagg: int) -> str:
129
+ if hw.Huawei:
130
+ return f"Eth-Trunk{nlagg}"
131
+ if hw.Cisco:
132
+ return f"port-channel{nlagg}"
133
+ if hw.Nexus:
134
+ return f"port-channel{nlagg}"
135
+ if hw.Arista:
136
+ return f"Port-Channel{nlagg}"
137
+ if hw.Juniper:
138
+ return f"ae{nlagg}"
139
+ if hw.Nokia:
140
+ return f"lag-{nlagg}"
141
+ if hw.PC.Whitebox:
142
+ return f"bond{nlagg}"
143
+ if hw.PC:
144
+ return f"lagg{nlagg}"
145
+ if hw.Nokia:
146
+ return f"lagg-{nlagg}"
147
+ raise NotImplementedError(hw)
148
+
149
+
150
+ def svi_name(hw: HardwareView, num: int) -> str:
151
+ if hw.Juniper:
152
+ return f"irb.{num}"
153
+ elif hw.Huawei:
154
+ return f"Vlanif{num}"
155
+ else:
156
+ return f"vlan{num}"