annet 0.5__py3-none-any.whl → 0.7__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.
- annet/adapters/__init__.py +0 -0
- annet/adapters/netbox/__init__.py +0 -0
- annet/adapters/netbox/common/__init__.py +0 -0
- annet/adapters/netbox/common/client.py +87 -0
- annet/adapters/netbox/common/manufacturer.py +62 -0
- annet/adapters/netbox/common/models.py +98 -0
- annet/adapters/netbox/common/query.py +23 -0
- annet/adapters/netbox/common/status_client.py +24 -0
- annet/adapters/netbox/common/storage_opts.py +14 -0
- annet/adapters/netbox/provider.py +34 -0
- annet/adapters/netbox/v24/__init__.py +0 -0
- annet/adapters/netbox/v24/api_models.py +72 -0
- annet/adapters/netbox/v24/client.py +59 -0
- annet/adapters/netbox/v24/storage.py +190 -0
- annet/adapters/netbox/v37/__init__.py +0 -0
- annet/adapters/netbox/v37/api_models.py +37 -0
- annet/adapters/netbox/v37/client.py +62 -0
- annet/adapters/netbox/v37/storage.py +143 -0
- annet/annlib/filter_acl.py +1 -1
- annet/api/__init__.py +27 -16
- annet/cli.py +6 -2
- annet/cli_args.py +10 -0
- annet/deploy.py +96 -1
- annet/diff.py +1 -2
- annet/gen.py +7 -3
- annet/generators/__init__.py +7 -9
- annet/output.py +3 -1
- annet/rulebook/aruba/ap_env.py +3 -3
- annet/rulebook/texts/huawei.rul +2 -2
- {annet-0.5.dist-info → annet-0.7.dist-info}/METADATA +3 -1
- {annet-0.5.dist-info → annet-0.7.dist-info}/RECORD +37 -21
- {annet-0.5.dist-info → annet-0.7.dist-info}/WHEEL +1 -1
- annet-0.7.dist-info/entry_points.txt +5 -0
- {annet-0.5.dist-info → annet-0.7.dist-info}/top_level.txt +0 -1
- annet_generators/example/lldp.py +1 -0
- annet-0.5.dist-info/entry_points.txt +0 -6
- annet_nbexport/__init__.py +0 -220
- annet_nbexport/main.py +0 -46
- {annet-0.5.dist-info → annet-0.7.dist-info}/AUTHORS +0 -0
- {annet-0.5.dist-info → annet-0.7.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any
|
|
4
|
+
|
|
5
|
+
from annet.adapters.netbox.common.models import (
|
|
6
|
+
Entity, Label, DeviceType, DeviceIp,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Interface(Entity):
|
|
12
|
+
device: Entity
|
|
13
|
+
enabled: bool
|
|
14
|
+
display: str = "" # added in 3.x
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Device(Entity):
|
|
19
|
+
display: str # renamed in 3.x from display_name
|
|
20
|
+
device_type: DeviceType
|
|
21
|
+
device_role: Entity
|
|
22
|
+
tenant: Optional[Entity]
|
|
23
|
+
platform: Optional[Entity]
|
|
24
|
+
serial: str
|
|
25
|
+
asset_tag: Optional[str]
|
|
26
|
+
site: Entity
|
|
27
|
+
rack: Optional[Entity]
|
|
28
|
+
position: Optional[float]
|
|
29
|
+
face: Optional[Label]
|
|
30
|
+
status: Label
|
|
31
|
+
primary_ip: Optional[DeviceIp]
|
|
32
|
+
primary_ip4: Optional[DeviceIp]
|
|
33
|
+
primary_ip6: Optional[DeviceIp]
|
|
34
|
+
tags: List[Entity]
|
|
35
|
+
custom_fields: dict[str, Any]
|
|
36
|
+
created: datetime
|
|
37
|
+
last_updated: datetime
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import dateutil.parser
|
|
5
|
+
from adaptix import Retort, loader
|
|
6
|
+
from dataclass_rest import get
|
|
7
|
+
from dataclass_rest.client_protocol import FactoryProtocol
|
|
8
|
+
|
|
9
|
+
from annet.adapters.netbox.common.client import (
|
|
10
|
+
BaseNetboxClient, collect, PagingResponse,
|
|
11
|
+
)
|
|
12
|
+
from annet.adapters.netbox.common.models import IpAddress
|
|
13
|
+
from .api_models import Device, Interface
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NetboxV37(BaseNetboxClient):
|
|
17
|
+
def _init_response_body_factory(self) -> FactoryProtocol:
|
|
18
|
+
return Retort(recipe=[
|
|
19
|
+
loader(datetime, dateutil.parser.parse)
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
@get("dcim/interfaces")
|
|
23
|
+
def interfaces(
|
|
24
|
+
self,
|
|
25
|
+
device_id: Optional[List[int]] = None,
|
|
26
|
+
limit: int = 20,
|
|
27
|
+
offset: int = 0,
|
|
28
|
+
) -> PagingResponse[Interface]:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
all_interfaces = collect(interfaces, field="device_id")
|
|
32
|
+
|
|
33
|
+
@get("ipam/ip-addresses")
|
|
34
|
+
def ip_addresses(
|
|
35
|
+
self,
|
|
36
|
+
interface_id: Optional[List[int]] = None,
|
|
37
|
+
limit: int = 20,
|
|
38
|
+
offset: int = 0,
|
|
39
|
+
) -> PagingResponse[IpAddress]:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
all_ip_addresses = collect(ip_addresses, field="interface_id")
|
|
43
|
+
|
|
44
|
+
@get("dcim/devices")
|
|
45
|
+
def devices(
|
|
46
|
+
self,
|
|
47
|
+
name: Optional[List[str]] = None,
|
|
48
|
+
name__ic: Optional[List[str]] = None,
|
|
49
|
+
tag: Optional[List[str]] = None,
|
|
50
|
+
limit: int = 20,
|
|
51
|
+
offset: int = 0,
|
|
52
|
+
) -> PagingResponse[Device]:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
all_devices = collect(devices)
|
|
56
|
+
|
|
57
|
+
@get("dcim/devices/{device_id}")
|
|
58
|
+
def get_device(
|
|
59
|
+
self,
|
|
60
|
+
device_id: int,
|
|
61
|
+
) -> Device:
|
|
62
|
+
pass
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from adaptix import P
|
|
5
|
+
from adaptix.conversion import impl_converter, link
|
|
6
|
+
|
|
7
|
+
from annet.adapters.netbox.common import models
|
|
8
|
+
from annet.adapters.netbox.common.manufacturer import (
|
|
9
|
+
is_supported, get_hw, get_breed,
|
|
10
|
+
)
|
|
11
|
+
from annet.adapters.netbox.common.query import NetboxQuery
|
|
12
|
+
from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
|
|
13
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
14
|
+
from annet.storage import Storage
|
|
15
|
+
from . import api_models
|
|
16
|
+
from .client import NetboxV37
|
|
17
|
+
|
|
18
|
+
logger = getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@impl_converter(recipe=[
|
|
22
|
+
link(P[api_models.Device].name, P[models.NetboxDevice].hostname),
|
|
23
|
+
link(P[api_models.Device].name, P[models.NetboxDevice].fqdn),
|
|
24
|
+
])
|
|
25
|
+
def extend_device_base(
|
|
26
|
+
device: api_models.Device,
|
|
27
|
+
interfaces: List[models.Interface],
|
|
28
|
+
hw: Optional[HardwareView],
|
|
29
|
+
breed: str,
|
|
30
|
+
neighbours_ids: List[int],
|
|
31
|
+
) -> models.NetboxDevice:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def extend_device(
|
|
36
|
+
device: api_models.Device,
|
|
37
|
+
) -> models.NetboxDevice:
|
|
38
|
+
return extend_device_base(
|
|
39
|
+
device=device,
|
|
40
|
+
interfaces=[],
|
|
41
|
+
breed=get_breed(
|
|
42
|
+
device.device_type.manufacturer.name,
|
|
43
|
+
device.device_type.model,
|
|
44
|
+
),
|
|
45
|
+
hw=get_hw(
|
|
46
|
+
device.device_type.manufacturer.name,
|
|
47
|
+
device.device_type.model,
|
|
48
|
+
),
|
|
49
|
+
neighbours_ids=[],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@impl_converter
|
|
54
|
+
def extend_interface(
|
|
55
|
+
interface: api_models.Interface, ip_addresses: List[models.IpAddress],
|
|
56
|
+
) -> models.Interface:
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NetboxStorageV37(Storage):
|
|
61
|
+
def __init__(self, opts: Optional[NetboxStorageOpts] = None):
|
|
62
|
+
self.netbox = NetboxV37(
|
|
63
|
+
url=opts.url,
|
|
64
|
+
token=opts.token,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __enter__(self):
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(self, _, __, ___):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def resolve_object_ids_by_query(self, query: NetboxQuery):
|
|
74
|
+
return [
|
|
75
|
+
d.id for d in self._load_devices(query)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
def resolve_fdnds_by_query(self, query: NetboxQuery):
|
|
79
|
+
return [
|
|
80
|
+
d.name for d in self._load_devices(query)
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
def make_devices(
|
|
84
|
+
self,
|
|
85
|
+
query: NetboxQuery,
|
|
86
|
+
preload_neighbors=False,
|
|
87
|
+
use_mesh=None,
|
|
88
|
+
preload_extra_fields=False,
|
|
89
|
+
**kwargs,
|
|
90
|
+
) -> List[models.NetboxDevice]:
|
|
91
|
+
device_ids = {
|
|
92
|
+
device.id: extend_device(device=device)
|
|
93
|
+
for device in self._load_devices(query)
|
|
94
|
+
}
|
|
95
|
+
if not device_ids:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
interfaces = self._load_interfaces(list(device_ids))
|
|
99
|
+
for interface in interfaces:
|
|
100
|
+
device_ids[interface.device.id].interfaces.append(interface)
|
|
101
|
+
return list(device_ids.values())
|
|
102
|
+
|
|
103
|
+
def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
|
|
104
|
+
return [
|
|
105
|
+
device
|
|
106
|
+
for device in self.netbox.all_devices(
|
|
107
|
+
name__ic=query.globs,
|
|
108
|
+
).results
|
|
109
|
+
if _match_query(query, device)
|
|
110
|
+
if is_supported(device.device_type.manufacturer.name)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def _load_interfaces(self, device_ids: List[int]) -> List[
|
|
114
|
+
models.Interface]:
|
|
115
|
+
interfaces = self.netbox.all_interfaces(device_id=device_ids)
|
|
116
|
+
extended_ifaces = {
|
|
117
|
+
interface.id: extend_interface(interface, [])
|
|
118
|
+
for interface in interfaces.results
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ips = self.netbox.all_ip_addresses(interface_id=list(extended_ifaces))
|
|
122
|
+
for ip in ips.results:
|
|
123
|
+
extended_ifaces[ip.assigned_object_id].ip_addresses.append(ip)
|
|
124
|
+
return list(extended_ifaces.values())
|
|
125
|
+
|
|
126
|
+
def get_device(
|
|
127
|
+
self, obj_id, preload_neighbors=False, use_mesh=None,
|
|
128
|
+
**kwargs,
|
|
129
|
+
) -> models.NetboxDevice:
|
|
130
|
+
device = self.netbox.get_device(obj_id)
|
|
131
|
+
res = extend_device(device=device)
|
|
132
|
+
res.interfaces = self._load_interfaces([device.id])
|
|
133
|
+
return res
|
|
134
|
+
|
|
135
|
+
def flush_perf(self):
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _match_query(query: NetboxQuery, device_data: api_models.Device) -> bool:
|
|
140
|
+
for subquery in query.globs:
|
|
141
|
+
if subquery.strip() in device_data.name:
|
|
142
|
+
return True
|
|
143
|
+
return False
|
annet/annlib/filter_acl.py
CHANGED
|
@@ -116,7 +116,7 @@ def get_op(line: str) -> typing.Tuple[str, str, str]:
|
|
|
116
116
|
break
|
|
117
117
|
if opidx >= 0:
|
|
118
118
|
op = line[opidx]
|
|
119
|
-
indent = line[:opidx] + line[opidx+1:rowstart]
|
|
119
|
+
indent = line[:opidx] + line[opidx + 1:rowstart]
|
|
120
120
|
if op != " ":
|
|
121
121
|
indent = indent + " "
|
|
122
122
|
return op, indent, line[rowstart:]
|
annet/api/__init__.py
CHANGED
|
@@ -188,7 +188,9 @@ def _print_pre_as_diff(pre, show_rules, indent, file=None, _level=0):
|
|
|
188
188
|
def log_host_progress_cb(pool: Parallel, task_result: TaskResult):
|
|
189
189
|
progress_logger = get_logger("progress")
|
|
190
190
|
args = cast(cli_args.QueryOptions, pool.args[0])
|
|
191
|
-
|
|
191
|
+
connector = storage_connector.get()
|
|
192
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
193
|
+
with connector.storage()(storage_opts) as storage:
|
|
192
194
|
hosts = storage.resolve_fdnds_by_query(args.query)
|
|
193
195
|
perc = int(pool.tasks_done / len(hosts) * 100)
|
|
194
196
|
fqdn = hosts[task_result.device_id]
|
|
@@ -210,7 +212,9 @@ def log_host_progress_cb(pool: Parallel, task_result: TaskResult):
|
|
|
210
212
|
# =====
|
|
211
213
|
def gen(args: cli_args.ShowGenOptions):
|
|
212
214
|
""" Сгенерировать конфиг для устройств """
|
|
213
|
-
|
|
215
|
+
connector = storage_connector.get()
|
|
216
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
217
|
+
with connector.storage()(storage_opts) as storage:
|
|
214
218
|
loader = ann_gen.Loader(storage, args)
|
|
215
219
|
stdin = args.stdin(storage=storage, filter_acl=args.filter_acl, config=None)
|
|
216
220
|
|
|
@@ -243,7 +247,9 @@ def _diff_files(old_files, new_files, context=3):
|
|
|
243
247
|
def patch(args: cli_args.ShowPatchOptions):
|
|
244
248
|
""" Сгенерировать патч для устройств """
|
|
245
249
|
global live_configs # pylint: disable=global-statement
|
|
246
|
-
|
|
250
|
+
connector = storage_connector.get()
|
|
251
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
252
|
+
with connector.storage()(storage_opts) as storage:
|
|
247
253
|
loader = ann_gen.Loader(storage, args)
|
|
248
254
|
if args.config == "running":
|
|
249
255
|
fetcher = annet.deploy.fetcher_connector.get()
|
|
@@ -286,7 +292,9 @@ def _patch_worker(device_id, args: cli_args.ShowPatchOptions, stdin, loader: ann
|
|
|
286
292
|
# =====
|
|
287
293
|
def res_diff_patch(device_id, args: cli_args.ShowPatchOptions, stdin, loader: ann_gen.Loader, filterer: filtering.Filterer) -> Iterable[
|
|
288
294
|
Tuple[OldNewResult, Dict, Dict]]:
|
|
289
|
-
|
|
295
|
+
connector = storage_connector.get()
|
|
296
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
297
|
+
with connector.storage()(storage_opts) as storage:
|
|
290
298
|
for res in ann_gen.old_new(
|
|
291
299
|
args,
|
|
292
300
|
storage,
|
|
@@ -314,7 +322,9 @@ def res_diff_patch(device_id, args: cli_args.ShowPatchOptions, stdin, loader: an
|
|
|
314
322
|
|
|
315
323
|
def diff(args: cli_args.DiffOptions, loader: ann_gen.Loader, filterer: filtering.Filterer) -> Mapping[Device, Union[Diff, PCDiff]]:
|
|
316
324
|
ret = {}
|
|
317
|
-
|
|
325
|
+
connector = storage_connector.get()
|
|
326
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
327
|
+
with connector.storage()(storage_opts) as storage:
|
|
318
328
|
for res in ann_gen.old_new(
|
|
319
329
|
args,
|
|
320
330
|
storage,
|
|
@@ -331,16 +341,15 @@ def diff(args: cli_args.DiffOptions, loader: ann_gen.Loader, filterer: filtering
|
|
|
331
341
|
acl_rules = res.get_acl_rules(args.acl_safe)
|
|
332
342
|
new_files = res.get_new_files(args.acl_safe)
|
|
333
343
|
new_json_fragment_files = res.get_new_file_fragments()
|
|
344
|
+
pc_diff_files = []
|
|
334
345
|
if res.old_files or new_files:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
diff_files=list(_json_fragment_diff(device.hostname, res.old_json_fragment_files, new_json_fragment_files)),
|
|
343
|
-
)
|
|
346
|
+
pc_diff_files.extend(_pc_diff(device.hostname, res.old_files, new_files))
|
|
347
|
+
if res.old_json_fragment_files or new_json_fragment_files:
|
|
348
|
+
pc_diff_files.extend(_json_fragment_diff(device.hostname, res.old_json_fragment_files, new_json_fragment_files))
|
|
349
|
+
|
|
350
|
+
if pc_diff_files:
|
|
351
|
+
pc_diff_files.sort(key=lambda f: f.label)
|
|
352
|
+
ret[device] = PCDiff(hostname=device.hostname, diff_files=pc_diff_files)
|
|
344
353
|
elif old is not None:
|
|
345
354
|
rb = rulebook.get_rulebook(device.hw)
|
|
346
355
|
diff_tree = patching.make_diff(old, new, rb, [acl_rules, res.filter_acl_rules])
|
|
@@ -625,7 +634,9 @@ def deploy(args: cli_args.DeployOptions) -> ExitCode:
|
|
|
625
634
|
""" Сгенерировать конфиг для устройств и задеплоить его """
|
|
626
635
|
ret: ExitCode = 0
|
|
627
636
|
deployer = Deployer(args)
|
|
628
|
-
|
|
637
|
+
connector = storage_connector.get()
|
|
638
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
639
|
+
with connector.storage()(storage_opts) as storage:
|
|
629
640
|
global live_configs # pylint: disable=global-statement
|
|
630
641
|
loader = ann_gen.Loader(storage, args)
|
|
631
642
|
filterer = filtering.filterer_connector.get()
|
|
@@ -761,7 +772,7 @@ def _json_fragment_diff(
|
|
|
761
772
|
|
|
762
773
|
def guess_hw(config_text: str):
|
|
763
774
|
"""Пытаемся угадать вендора и hw на основе
|
|
764
|
-
текста конфига и
|
|
775
|
+
текста конфига и annet/rulebook/texts/*.rul"""
|
|
765
776
|
scores = {}
|
|
766
777
|
hw_provider = hardware_connector.get()
|
|
767
778
|
for vendor in VENDOR_REVERSES:
|
annet/cli.py
CHANGED
|
@@ -54,7 +54,9 @@ def show_current(args: cli_args.QueryOptions, config, arg_out: cli_args.FileOutO
|
|
|
54
54
|
entire_data = ""
|
|
55
55
|
yield (output_driver.entire_config_dest_path(device, entire_path), entire_data, False)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
connector = storage_connector.get()
|
|
58
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
59
|
+
with connector.storage()(storage_opts) as storage:
|
|
58
60
|
ids = storage.resolve_object_ids_by_query(args.query)
|
|
59
61
|
if not ids:
|
|
60
62
|
get_logger().error("No devices found for %s", args.query)
|
|
@@ -80,7 +82,9 @@ def gen(args: cli_args.ShowGenOptions):
|
|
|
80
82
|
@subcommand(cli_args.ShowDiffOptions)
|
|
81
83
|
def diff(args: cli_args.ShowDiffOptions):
|
|
82
84
|
""" Сгенерировать конфиг для устройств и показать дифф по рулбуку с текущим """
|
|
83
|
-
|
|
85
|
+
connector = storage_connector.get()
|
|
86
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
87
|
+
with connector.storage()(storage_opts) as storage:
|
|
84
88
|
filterer = filtering.filterer_connector.get()
|
|
85
89
|
loader = Loader(storage, args)
|
|
86
90
|
output_driver_connector.get().write_output(
|
annet/cli_args.py
CHANGED
|
@@ -433,6 +433,11 @@ class FileOutOptions(ArgGroup):
|
|
|
433
433
|
no_label = opt_no_label
|
|
434
434
|
no_color = opt_no_color
|
|
435
435
|
|
|
436
|
+
def __init__(self, *args, **kwargs):
|
|
437
|
+
super().__init__(*args, **kwargs)
|
|
438
|
+
if self.dest:
|
|
439
|
+
self.no_color = True
|
|
440
|
+
|
|
436
441
|
|
|
437
442
|
class DiffOptions(GenOptions, ComocutorOptions):
|
|
438
443
|
clear = opt_clear
|
|
@@ -467,6 +472,11 @@ class ShowDiffOptions(DiffOptions, FileOutOptions):
|
|
|
467
472
|
show_rules = opt_show_rules
|
|
468
473
|
no_collapse = opt_no_collapse
|
|
469
474
|
|
|
475
|
+
def __init__(self, *args, **kwargs):
|
|
476
|
+
super().__init__(*args, **kwargs)
|
|
477
|
+
if self.dest:
|
|
478
|
+
self.no_collapse = True
|
|
479
|
+
|
|
470
480
|
|
|
471
481
|
class ShowPatchOptions(PatchOptions, FileOutOptions):
|
|
472
482
|
indent = opt_indent
|
annet/deploy.py
CHANGED
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
import abc
|
|
5
|
+
import itertools
|
|
5
6
|
import re
|
|
6
7
|
from collections import namedtuple
|
|
7
8
|
from contextlib import contextmanager
|
|
8
|
-
from typing import Dict, List, Optional, Type
|
|
9
|
+
from typing import Dict, List, Optional, Type, Any, OrderedDict
|
|
9
10
|
|
|
10
11
|
from contextlog import get_logger
|
|
11
12
|
|
|
12
13
|
from annet import text_term_format
|
|
14
|
+
from annet.annlib.command import Command, Question, CommandList
|
|
15
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
16
|
+
from annet.annlib.rbparser.deploying import MakeMessageMatcher, Answer
|
|
13
17
|
from annet.cli_args import DeployOptions
|
|
14
18
|
from annet.connectors import Connector
|
|
15
19
|
from annet.output import TextArgs
|
|
20
|
+
from annet.rulebook import get_rulebook, deploying
|
|
16
21
|
from annet.storage import Device
|
|
17
22
|
|
|
18
23
|
|
|
@@ -439,3 +444,93 @@ def init_colors():
|
|
|
439
444
|
None: curses.color_pair(8),
|
|
440
445
|
"cyan_blue": curses.color_pair(9),
|
|
441
446
|
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class RulebookQuestionHandler:
|
|
450
|
+
def __init__(self, dialogs):
|
|
451
|
+
self._dialogs = dialogs
|
|
452
|
+
|
|
453
|
+
def __call__(self, dev: Connector, cmd: Command, match_content: bytes):
|
|
454
|
+
content = match_content.strip()
|
|
455
|
+
content = content.decode()
|
|
456
|
+
for matcher, answer in self._dialogs.items():
|
|
457
|
+
if matcher(content):
|
|
458
|
+
return Command(answer.text)
|
|
459
|
+
|
|
460
|
+
get_logger().info("no answer in rulebook. dialogs=%s match_content=%s", self._dialogs, match_content)
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def rb_question_to_question(q: MakeMessageMatcher, a: Answer) -> Question: # TODO: drop MakeMessageMatcher
|
|
465
|
+
if not a.send_nl:
|
|
466
|
+
raise Exception("not supported false send_nl")
|
|
467
|
+
text: str = q._text # pylint: disable=protected-access
|
|
468
|
+
is_regexp = False
|
|
469
|
+
if text.startswith("/") and text.endswith("/"):
|
|
470
|
+
is_regexp = True
|
|
471
|
+
text = text[1:-1]
|
|
472
|
+
res = Question(question=text, answer=a.text, is_regexp=is_regexp)
|
|
473
|
+
return res
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def make_cmd_params(rule: Dict[str, Any]) -> Dict[str, Any]:
|
|
477
|
+
if rule:
|
|
478
|
+
qa_handler = RulebookQuestionHandler(rule["attrs"]["dialogs"])
|
|
479
|
+
qa_list: List[Question] = []
|
|
480
|
+
for matcher, answer in qa_handler._dialogs.items(): # pylint: disable=protected-access
|
|
481
|
+
qa_list.append(rb_question_to_question(matcher, answer))
|
|
482
|
+
return {
|
|
483
|
+
"questions": qa_list,
|
|
484
|
+
"timeout": rule["attrs"]["timeout"],
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
"timeout": 30,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def make_apply_commands(rule, hw, do_commit, do_finalize):
|
|
492
|
+
apply_logic = rule["attrs"]["apply_logic"]
|
|
493
|
+
before, after = apply_logic(hw, do_commit=do_commit, do_finalize=do_finalize)
|
|
494
|
+
return before, after
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def fill_cmd_params(rules: OrderedDict, cmd: Command):
|
|
498
|
+
rule = deploying.match_deploy_rule(rules, (cmd.cmd,), {})
|
|
499
|
+
if rule:
|
|
500
|
+
cmd_params = make_cmd_params(rule)
|
|
501
|
+
cmd.questions = cmd_params.get("questions", None)
|
|
502
|
+
cmd.timeout = cmd_params["timeout"]
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def apply_deploy_rulebook(hw: HardwareView, cmd_paths, do_finalize=True, do_commit=True):
|
|
506
|
+
rules = get_rulebook(hw)["deploying"]
|
|
507
|
+
cmds_with_apply = []
|
|
508
|
+
for cmd_path, context in cmd_paths.items():
|
|
509
|
+
rule = deploying.match_deploy_rule(rules, cmd_path, context)
|
|
510
|
+
cmd_params = make_cmd_params(rule)
|
|
511
|
+
before, after = make_apply_commands(rule, hw, do_commit, do_finalize)
|
|
512
|
+
|
|
513
|
+
cmd = Command(cmd_path[-1], **cmd_params)
|
|
514
|
+
# XXX более чистый способ передавать-мета инфу о команде
|
|
515
|
+
cmd.level = len(cmd_path) - 1
|
|
516
|
+
cmds_with_apply.append((cmd, before, after))
|
|
517
|
+
|
|
518
|
+
def _key(item):
|
|
519
|
+
_cmd, before, after = item
|
|
520
|
+
return (tuple(cmd.cmd for cmd in before), tuple(cmd.cmd for cmd in after))
|
|
521
|
+
|
|
522
|
+
cmdlist = CommandList()
|
|
523
|
+
for _k, cmd_before_after in itertools.groupby(cmds_with_apply, key=_key):
|
|
524
|
+
cmd_before_after = list(cmd_before_after)
|
|
525
|
+
_, before, after = cmd_before_after[0]
|
|
526
|
+
for c in before:
|
|
527
|
+
c.level = 0
|
|
528
|
+
fill_cmd_params(rules, c)
|
|
529
|
+
cmdlist.add_cmd(c)
|
|
530
|
+
for cmd, _before, _after in cmd_before_after:
|
|
531
|
+
cmdlist.add_cmd(cmd)
|
|
532
|
+
for c in after:
|
|
533
|
+
c.level = 0
|
|
534
|
+
fill_cmd_params(rules, c)
|
|
535
|
+
cmdlist.add_cmd(c)
|
|
536
|
+
return cmdlist
|
annet/diff.py
CHANGED
|
@@ -30,8 +30,7 @@ def gen_sort_diff(
|
|
|
30
30
|
:param diffs: Маппинг устройства в дифф
|
|
31
31
|
:param args: Параметры коммандной строки
|
|
32
32
|
"""
|
|
33
|
-
|
|
34
|
-
if args.no_collapse or args.dest:
|
|
33
|
+
if args.no_collapse:
|
|
35
34
|
devices_to_diff = {(dev,): diff for dev, diff in diffs.items()}
|
|
36
35
|
else:
|
|
37
36
|
non_pc_diffs = {dev: diff for dev, diff in diffs.items() if not isinstance(diff, PCDiff)}
|
annet/gen.py
CHANGED
|
@@ -425,7 +425,9 @@ def worker(device_id, args: ShowGenOptions, stdin, loader: "Loader", filterer: F
|
|
|
425
425
|
if span:
|
|
426
426
|
span.set_attribute("device.id", device_id)
|
|
427
427
|
|
|
428
|
-
|
|
428
|
+
connector = storage_connector.get()
|
|
429
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
430
|
+
with connector.storage()(storage_opts) as storage:
|
|
429
431
|
for res in old_new(
|
|
430
432
|
args,
|
|
431
433
|
storage,
|
|
@@ -465,7 +467,9 @@ def worker(device_id, args: ShowGenOptions, stdin, loader: "Loader", filterer: F
|
|
|
465
467
|
|
|
466
468
|
|
|
467
469
|
def old_new_worker(device_id, args: DeployOptions, config, stdin, loader: "Loader", filterer: Filterer):
|
|
468
|
-
|
|
470
|
+
connector = storage_connector.get()
|
|
471
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
472
|
+
with connector.storage()(storage_opts) as storage:
|
|
469
473
|
yield from old_new(
|
|
470
474
|
args,
|
|
471
475
|
storage,
|
|
@@ -693,7 +697,7 @@ def _old_new_get_config_cli(ctx: OldNewDeviceContext, device: Device) -> str:
|
|
|
693
697
|
def _old_new_get_config_files(ctx: OldNewDeviceContext, device: Device) -> DeviceDownloadedFiles:
|
|
694
698
|
old_files = DeviceDownloadedFiles()
|
|
695
699
|
|
|
696
|
-
for attr in (
|
|
700
|
+
for attr in ("failed_files", "failed_running", "failed_packages"):
|
|
697
701
|
if device in getattr(ctx, attr):
|
|
698
702
|
exc = getattr(ctx, attr).get(device)
|
|
699
703
|
exc = exc or Exception(f"I can't get device {attr[len('failed_'):]} and I don't know why")
|
annet/generators/__init__.py
CHANGED
|
@@ -538,15 +538,7 @@ def _get_generators(module_paths: Union[List[str], dict], storage, device=None):
|
|
|
538
538
|
module = importlib.import_module(module_path)
|
|
539
539
|
if hasattr(module, "get_generators"):
|
|
540
540
|
generators: List[BaseGenerator] = module.get_generators(storage)
|
|
541
|
-
|
|
542
|
-
res_generators += generators
|
|
543
|
-
else:
|
|
544
|
-
logger = get_logger()
|
|
545
|
-
for gen in generators:
|
|
546
|
-
if gen.supports_vendor(device.hw.vendor):
|
|
547
|
-
res_generators.append(gen)
|
|
548
|
-
else:
|
|
549
|
-
logger.info("generator %s does not support device vendor %s, skipping", gen, device.hw.vendor)
|
|
541
|
+
res_generators += generators
|
|
550
542
|
return res_generators
|
|
551
543
|
|
|
552
544
|
|
|
@@ -706,6 +698,12 @@ class PartialGenerator(TreeGenerator):
|
|
|
706
698
|
def run(self, device) -> Iterable[Union[str, tuple]]:
|
|
707
699
|
if hasattr(self, "run_" + device.hw.vendor):
|
|
708
700
|
return getattr(self, "run_" + device.hw.vendor)(device)
|
|
701
|
+
logger = get_logger()
|
|
702
|
+
logger.info(
|
|
703
|
+
"generator %s is not supported for vendor %s",
|
|
704
|
+
self,
|
|
705
|
+
device.hw.vendor,
|
|
706
|
+
)
|
|
709
707
|
return iter(())
|
|
710
708
|
|
|
711
709
|
def get_user_runner(self, device):
|
annet/output.py
CHANGED
|
@@ -133,7 +133,9 @@ class OutputDriverBasic(OutputDriver):
|
|
|
133
133
|
ret = []
|
|
134
134
|
fqdns = {}
|
|
135
135
|
if args:
|
|
136
|
-
|
|
136
|
+
connector = storage_connector.get()
|
|
137
|
+
storage_opts = connector.opts().from_cli_opts(args)
|
|
138
|
+
with connector.storage()(storage_opts) as storage:
|
|
137
139
|
fqdns = storage.resolve_fdnds_by_query(args.query)
|
|
138
140
|
for (assignment, exc) in fail.items():
|
|
139
141
|
label = assignment
|
annet/rulebook/aruba/ap_env.py
CHANGED
|
@@ -4,15 +4,15 @@ from annet.annlib.types import Op
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
try:
|
|
7
|
-
from annet.executor import CommandList
|
|
7
|
+
from annet.executor import CommandList, Command
|
|
8
8
|
except ImportError:
|
|
9
|
-
from noc.annushka.annet.executor import CommandList
|
|
9
|
+
from noc.annushka.annet.executor import CommandList, Command
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def apply(hw, do_commit, do_finalize, **_):
|
|
13
13
|
before, after = CommandList(), CommandList()
|
|
14
14
|
if do_commit:
|
|
15
|
-
after.add_cmd("write memory")
|
|
15
|
+
after.add_cmd(Command("write memory"))
|
|
16
16
|
return (before, after)
|
|
17
17
|
|
|
18
18
|
|
annet/rulebook/texts/huawei.rul
CHANGED
|
@@ -325,7 +325,7 @@ interface * %logic=huawei.iface.permanent
|
|
|
325
325
|
trust upstream
|
|
326
326
|
%if hw.Huawei.CE:
|
|
327
327
|
trust *
|
|
328
|
-
%else
|
|
328
|
+
%else:
|
|
329
329
|
trust
|
|
330
330
|
%endif
|
|
331
331
|
dhcp snooping check dhcp-rate enable
|
|
@@ -442,7 +442,7 @@ snmp-agent trap source
|
|
|
442
442
|
snmp-agent packet max-size
|
|
443
443
|
%if hw.Huawei.Quidway:
|
|
444
444
|
snmp-agent protocol source-interface %logic=huawei.misc.undo_redo
|
|
445
|
-
%else
|
|
445
|
+
%else:
|
|
446
446
|
snmp-agent protocol source-interface *
|
|
447
447
|
%endif
|
|
448
448
|
snmp-agent ~
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: annet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7
|
|
4
4
|
Summary: annet
|
|
5
5
|
Home-page: https://github.com/annetutil/annet
|
|
6
6
|
License: MIT
|
|
@@ -21,4 +21,6 @@ Requires-Dist: contextlog >=1.1
|
|
|
21
21
|
Requires-Dist: valkit >=0.1.4
|
|
22
22
|
Requires-Dist: aiohttp >=3.8.4
|
|
23
23
|
Requires-Dist: yarl >=1.8.2
|
|
24
|
+
Requires-Dist: adaptix ==3.0.0b2
|
|
25
|
+
Requires-Dist: dataclass-rest ==0.4
|
|
24
26
|
|