annet 0.11__py3-none-any.whl → 0.12.1__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/netbox/common/models.py +10 -3
- annet/adapters/netbox/common/status_client.py +2 -1
- annet/adapters/netbox/v24/api_models.py +4 -3
- annet/adapters/netbox/v24/storage.py +12 -6
- annet/adapters/netbox/v37/api_models.py +3 -2
- annet/adapters/netbox/v37/storage.py +11 -5
- annet/api/__init__.py +167 -159
- annet/cli.py +96 -55
- annet/cli_args.py +15 -15
- annet/connectors.py +28 -15
- annet/gen.py +126 -129
- annet/generators/__init__.py +41 -627
- annet/generators/base.py +136 -0
- annet/generators/common/initial.py +1 -1
- annet/generators/entire.py +97 -0
- annet/generators/exceptions.py +10 -0
- annet/generators/jsonfragment.py +125 -0
- annet/generators/partial.py +119 -0
- annet/generators/perf.py +79 -0
- annet/generators/ref.py +15 -0
- annet/generators/result.py +127 -0
- annet/output.py +4 -9
- annet/storage.py +7 -3
- annet/types.py +0 -2
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/METADATA +1 -1
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/RECORD +31 -23
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/AUTHORS +0 -0
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/LICENSE +0 -0
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/WHEEL +0 -0
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/entry_points.txt +0 -0
- {annet-0.11.dist-info → annet-0.12.1.dist-info}/top_level.txt +0 -0
annet/generators/__init__.py
CHANGED
|
@@ -1,221 +1,53 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import abc
|
|
4
|
-
import contextlib
|
|
5
3
|
import dataclasses
|
|
6
4
|
import importlib
|
|
7
5
|
import os
|
|
8
|
-
import pkgutil
|
|
9
|
-
import re
|
|
10
6
|
import textwrap
|
|
11
|
-
import time
|
|
12
|
-
import types
|
|
13
7
|
from collections import OrderedDict as odict
|
|
14
8
|
from typing import (
|
|
15
|
-
Any,
|
|
16
|
-
Callable,
|
|
17
|
-
Dict,
|
|
18
9
|
FrozenSet,
|
|
19
10
|
Iterable,
|
|
20
11
|
List,
|
|
21
12
|
Optional,
|
|
22
|
-
Set,
|
|
23
|
-
Tuple,
|
|
24
13
|
Union,
|
|
25
14
|
)
|
|
26
15
|
|
|
27
|
-
from annet.annlib import jsontools
|
|
28
16
|
from annet.annlib.rbparser.acl import compile_acl_text
|
|
29
17
|
from contextlog import get_logger
|
|
30
18
|
|
|
31
|
-
from annet.storage import Device
|
|
19
|
+
from annet.storage import Device
|
|
32
20
|
|
|
33
21
|
from annet import patching, tabparser, tracing
|
|
34
22
|
from annet.cli_args import GenSelectOptions, ShowGeneratorsOptions
|
|
35
23
|
from annet.lib import (
|
|
36
|
-
add_annotation,
|
|
37
|
-
flatten,
|
|
38
24
|
get_context,
|
|
39
|
-
jinja_render,
|
|
40
|
-
mako_render,
|
|
41
|
-
merge_dicts,
|
|
42
25
|
)
|
|
43
|
-
from annet.reference import RefMatcher, RefTracker
|
|
44
26
|
from annet.tracing import tracing_connector
|
|
45
27
|
from annet.types import (
|
|
46
28
|
GeneratorEntireResult,
|
|
47
29
|
GeneratorJSONFragmentResult,
|
|
48
30
|
GeneratorPartialResult,
|
|
49
31
|
GeneratorPartialRunArgs,
|
|
50
|
-
GeneratorPerf,
|
|
51
32
|
GeneratorResult,
|
|
52
33
|
)
|
|
53
|
-
|
|
34
|
+
from .base import (
|
|
35
|
+
BaseGenerator,
|
|
36
|
+
TextGenerator as TextGenerator,
|
|
37
|
+
ParamsList as ParamsList,
|
|
38
|
+
)
|
|
39
|
+
from .exceptions import NotSupportedDevice, GeneratorError
|
|
40
|
+
from .jsonfragment import JSONFragment
|
|
41
|
+
from .partial import PartialGenerator
|
|
42
|
+
from .entire import Entire
|
|
43
|
+
from .ref import RefGenerator
|
|
44
|
+
from .perf import GeneratorPerfMesurer
|
|
45
|
+
from .result import RunGeneratorResult
|
|
54
46
|
|
|
55
47
|
# =====
|
|
56
48
|
DISABLED_TAG = "disable"
|
|
57
49
|
|
|
58
50
|
|
|
59
|
-
# =====
|
|
60
|
-
class GeneratorError(Exception):
|
|
61
|
-
pass
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class NotSupportedDevice(GeneratorError):
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class DefaultBlockIfCondition:
|
|
69
|
-
pass
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class GeneratorPerfMesurer:
|
|
73
|
-
def __init__(
|
|
74
|
-
self,
|
|
75
|
-
gen: Union["PartialGenerator", "Entire"],
|
|
76
|
-
storage: Storage,
|
|
77
|
-
run_args: Optional[GeneratorPartialRunArgs] = None,
|
|
78
|
-
trace_min_duration: tracing.MinDurationT = None
|
|
79
|
-
):
|
|
80
|
-
self._gen = gen
|
|
81
|
-
self._storage = storage
|
|
82
|
-
self._run_args = run_args
|
|
83
|
-
|
|
84
|
-
self._start_time: float = 0.0
|
|
85
|
-
self._span_ctx = None
|
|
86
|
-
self._span = None
|
|
87
|
-
self._trace_min_duration = trace_min_duration
|
|
88
|
-
|
|
89
|
-
self.last_result: Optional[GeneratorPerf] = None
|
|
90
|
-
|
|
91
|
-
def start(self) -> None:
|
|
92
|
-
self.last_result = None
|
|
93
|
-
|
|
94
|
-
self._storage.flush_perf()
|
|
95
|
-
|
|
96
|
-
self._span_ctx = tracing_connector.get().start_as_current_span(
|
|
97
|
-
"gen:call",
|
|
98
|
-
tracer_name=self._gen.__class__.__module__,
|
|
99
|
-
min_duration=self._trace_min_duration,
|
|
100
|
-
)
|
|
101
|
-
self._span = self._span_ctx.__enter__() # pylint: disable=unnecessary-dunder-call
|
|
102
|
-
|
|
103
|
-
if self._span:
|
|
104
|
-
self._span.set_attributes({"generator.class": self._gen.__class__.__name__})
|
|
105
|
-
if self._run_args:
|
|
106
|
-
tracing_connector.get().set_device_attributes(self._span, self._run_args.device)
|
|
107
|
-
|
|
108
|
-
self._start_time = time.monotonic()
|
|
109
|
-
|
|
110
|
-
def finish(self, exc_type=None, exc_val=None, exc_tb=None) -> GeneratorPerf:
|
|
111
|
-
total = time.monotonic() - self._start_time
|
|
112
|
-
rt = self._storage.flush_perf()
|
|
113
|
-
self._span_ctx.__exit__(exc_type, exc_val, exc_tb)
|
|
114
|
-
|
|
115
|
-
meta = {}
|
|
116
|
-
if tracing_connector.get().enabled:
|
|
117
|
-
span_context = self._span.get_span_context()
|
|
118
|
-
meta = {
|
|
119
|
-
"span": {
|
|
120
|
-
"trace_id": str(span_context.trace_id),
|
|
121
|
-
"span_id": str(span_context.span_id),
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
self.last_result = GeneratorPerf(total=total, rt=rt, meta=meta)
|
|
126
|
-
return self.last_result
|
|
127
|
-
|
|
128
|
-
def __enter__(self):
|
|
129
|
-
self.start()
|
|
130
|
-
return self
|
|
131
|
-
|
|
132
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
133
|
-
self.finish(exc_type, exc_val, exc_tb)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
class RunGeneratorResult:
|
|
137
|
-
"""
|
|
138
|
-
Результат запуска run_partial_generators/run_file_generators
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
|
-
def __init__(self):
|
|
142
|
-
self.partial_results: Dict[str, GeneratorPartialResult] = {}
|
|
143
|
-
self.entire_results: Dict[str, GeneratorEntireResult] = {}
|
|
144
|
-
self.json_fragment_results: Dict[str, GeneratorJSONFragmentResult] = {}
|
|
145
|
-
self.ref_track: RefTracker = RefTracker()
|
|
146
|
-
self.ref_matcher: RefMatcher = RefMatcher()
|
|
147
|
-
|
|
148
|
-
def add_partial(self, result: GeneratorPartialResult):
|
|
149
|
-
self.partial_results[result.name] = result
|
|
150
|
-
|
|
151
|
-
def add_entire(self, result: GeneratorEntireResult) -> None:
|
|
152
|
-
# Если есть несколько генераторов на один файл, выбрать тот, что с большим приоритетом
|
|
153
|
-
if result.path:
|
|
154
|
-
if result.path not in self.entire_results or result.prio > self.entire_results[result.path].prio:
|
|
155
|
-
self.entire_results[result.path] = result
|
|
156
|
-
|
|
157
|
-
def add_json_fragment(self, result: GeneratorJSONFragmentResult) -> None:
|
|
158
|
-
self.json_fragment_results[result.name] = result
|
|
159
|
-
|
|
160
|
-
def config_tree(self, safe: bool = False) -> Dict[str, Any]: # OrderedDict
|
|
161
|
-
tree = odict()
|
|
162
|
-
for gr in self.partial_results.values():
|
|
163
|
-
config = gr.safe_config if safe else gr.config
|
|
164
|
-
tree = merge_dicts(tree, config)
|
|
165
|
-
return tree
|
|
166
|
-
|
|
167
|
-
def new_files(self, safe: bool = False) -> Dict[str, Tuple[str, str]]:
|
|
168
|
-
files = {}
|
|
169
|
-
for gr in self.entire_results.values():
|
|
170
|
-
if not safe or gr.is_safe:
|
|
171
|
-
files[gr.path] = (gr.output, gr.reload)
|
|
172
|
-
return files
|
|
173
|
-
|
|
174
|
-
def acl_text(self) -> str:
|
|
175
|
-
return _combine_acl_text(self.partial_results, lambda gr: gr.acl)
|
|
176
|
-
|
|
177
|
-
def acl_safe_text(self) -> str:
|
|
178
|
-
return _combine_acl_text(self.partial_results, lambda gr: gr.acl_safe)
|
|
179
|
-
|
|
180
|
-
def new_json_fragment_files(
|
|
181
|
-
self,
|
|
182
|
-
old_files: Dict[str, Optional[str]],
|
|
183
|
-
safe: bool = False,
|
|
184
|
-
) -> Dict[str, Tuple[Any, Optional[str]]]:
|
|
185
|
-
files: Dict[str, Tuple[Any, Optional[str]]] = {}
|
|
186
|
-
reload_prios: Dict[str, int] = {}
|
|
187
|
-
for generator_result in self.json_fragment_results.values():
|
|
188
|
-
filepath = generator_result.path
|
|
189
|
-
if filepath not in files:
|
|
190
|
-
if old_files.get(filepath) is not None:
|
|
191
|
-
files[filepath] = (old_files[filepath], None)
|
|
192
|
-
else:
|
|
193
|
-
files[filepath] = ({}, None)
|
|
194
|
-
previous_config: Dict[str, Any] = files[filepath][0]
|
|
195
|
-
new_fragment = generator_result.config
|
|
196
|
-
|
|
197
|
-
if safe:
|
|
198
|
-
new_config = jsontools.apply_json_fragment(previous_config, new_fragment, generator_result.acl_safe)
|
|
199
|
-
else:
|
|
200
|
-
new_config = jsontools.apply_json_fragment(previous_config, new_fragment, generator_result.acl)
|
|
201
|
-
|
|
202
|
-
if filepath in reload_prios and reload_prios[filepath] > generator_result.reload_prio:
|
|
203
|
-
_, reload_cmd = files[filepath]
|
|
204
|
-
else:
|
|
205
|
-
reload_cmd = generator_result.reload
|
|
206
|
-
reload_prios[filepath] = generator_result.reload_prio
|
|
207
|
-
files[filepath] = (new_config, reload_cmd)
|
|
208
|
-
return files
|
|
209
|
-
|
|
210
|
-
def perf_mesures(self) -> Dict[str, Dict[str, int]]:
|
|
211
|
-
mesures = {}
|
|
212
|
-
for gr in self.partial_results.values():
|
|
213
|
-
mesures[gr.name] = {"total": gr.perf.total, "rt": gr.perf.rt, "meta": gr.perf.meta}
|
|
214
|
-
for gr in self.entire_results.values():
|
|
215
|
-
mesures[gr.name] = {"total": gr.perf.total, "rt": gr.perf.rt, "meta": gr.perf.meta}
|
|
216
|
-
return mesures
|
|
217
|
-
|
|
218
|
-
|
|
219
51
|
# =====
|
|
220
52
|
def get_list(args: ShowGeneratorsOptions):
|
|
221
53
|
if args.generators_context is not None:
|
|
@@ -257,6 +89,7 @@ class Generators:
|
|
|
257
89
|
|
|
258
90
|
partial: List[PartialGenerator] = dataclasses.field(default_factory=list)
|
|
259
91
|
entire: List[Entire] = dataclasses.field(default_factory=list)
|
|
92
|
+
ref: List[RefGenerator] = dataclasses.field(default_factory=list)
|
|
260
93
|
json_fragment: List[JSONFragment] = dataclasses.field(default_factory=list)
|
|
261
94
|
|
|
262
95
|
|
|
@@ -265,35 +98,46 @@ def build_generators(storage, gens: GenSelectOptions, device: Optional[Device] =
|
|
|
265
98
|
if gens.generators_context is not None:
|
|
266
99
|
os.environ["ANN_GENERATORS_CONTEXT"] = gens.generators_context
|
|
267
100
|
all_generators = _get_generators(get_context()["generators"], storage, device)
|
|
101
|
+
ref_generators = _get_ref_generators(get_context()["generators"], storage, device)
|
|
268
102
|
validate_genselect(gens, all_generators)
|
|
269
103
|
classes = list(select_generators(gens, all_generators))
|
|
270
104
|
partial = [obj for obj in classes if obj.TYPE == "PARTIAL"]
|
|
271
105
|
entire = [obj for obj in classes if obj.TYPE == "ENTIRE"]
|
|
272
106
|
entire = list(sorted(entire, key=lambda x: x.prio, reverse=True))
|
|
273
107
|
json_fragment = [obj for obj in classes if obj.TYPE == "JSON_FRAGMENT"]
|
|
274
|
-
return Generators(
|
|
108
|
+
return Generators(
|
|
109
|
+
partial=partial,
|
|
110
|
+
entire=entire,
|
|
111
|
+
json_fragment=json_fragment,
|
|
112
|
+
ref=ref_generators,
|
|
113
|
+
)
|
|
275
114
|
|
|
276
115
|
|
|
277
116
|
@tracing.function
|
|
278
|
-
def run_partial_initial(device
|
|
117
|
+
def run_partial_initial(device):
|
|
279
118
|
from .common.initial import InitialConfig
|
|
280
119
|
|
|
281
120
|
tracing_connector.get().set_device_attributes(tracing_connector.get().get_current_span(), device)
|
|
282
121
|
|
|
283
|
-
run_args = GeneratorPartialRunArgs(device
|
|
284
|
-
return run_partial_generators([InitialConfig()], run_args)
|
|
122
|
+
run_args = GeneratorPartialRunArgs(device)
|
|
123
|
+
return run_partial_generators([InitialConfig(storage=device.storage)], [], run_args)
|
|
285
124
|
|
|
286
125
|
|
|
287
126
|
@tracing.function
|
|
288
|
-
def run_partial_generators(
|
|
127
|
+
def run_partial_generators(
|
|
128
|
+
gens: List["PartialGenerator"],
|
|
129
|
+
ref_gens: List["RefGenerator"],
|
|
130
|
+
run_args: GeneratorPartialRunArgs,
|
|
131
|
+
):
|
|
289
132
|
logger = get_logger(host=run_args.device.hostname)
|
|
290
133
|
tracing_connector.get().set_device_attributes(tracing_connector.get().get_current_span(), run_args.device)
|
|
291
134
|
|
|
292
135
|
ret = RunGeneratorResult()
|
|
293
136
|
if run_args.generators_context is not None:
|
|
294
137
|
os.environ["ANN_GENERATORS_CONTEXT"] = run_args.generators_context
|
|
295
|
-
|
|
296
|
-
|
|
138
|
+
|
|
139
|
+
for gen in ref_gens:
|
|
140
|
+
ret.ref_matcher.add(gen.ref(run_args.device), gen)
|
|
297
141
|
|
|
298
142
|
logger.debug("Generating selected PARTIALs ...")
|
|
299
143
|
|
|
@@ -310,9 +154,9 @@ def run_partial_generators(gens: List["PartialGenerator"], run_args: GeneratorPa
|
|
|
310
154
|
config = result.safe_config if run_args.use_acl_safe else result.config
|
|
311
155
|
|
|
312
156
|
ref_match = ret.ref_matcher.match(config)
|
|
313
|
-
for
|
|
314
|
-
gens.append(
|
|
315
|
-
ret.ref_track.add(gen.__class__,
|
|
157
|
+
for ref_gen, groups in ref_match:
|
|
158
|
+
gens.append(ref_gen.with_groups(groups))
|
|
159
|
+
ret.ref_track.add(gen.__class__, ref_gen.__class__)
|
|
316
160
|
|
|
317
161
|
ret.ref_track.config(gen.__class__, config)
|
|
318
162
|
ret.add_partial(result)
|
|
@@ -342,7 +186,7 @@ def _run_partial_generator(gen: "PartialGenerator", run_args: GeneratorPartialRu
|
|
|
342
186
|
"generators_context": str(run_args.generators_context),
|
|
343
187
|
})
|
|
344
188
|
|
|
345
|
-
with GeneratorPerfMesurer(gen, run_args
|
|
189
|
+
with GeneratorPerfMesurer(gen, run_args=run_args) as pm:
|
|
346
190
|
if not run_args.no_new:
|
|
347
191
|
if gen.get_user_runner(device):
|
|
348
192
|
logger.info("Generating PARTIAL ...")
|
|
@@ -430,7 +274,6 @@ def check_entire_generators_required_packages(gens, device_packages: FrozenSet[s
|
|
|
430
274
|
def run_file_generators(
|
|
431
275
|
gens: Iterable[Union["JSONFragment", "Entire"]],
|
|
432
276
|
device: "Device",
|
|
433
|
-
storage: Storage,
|
|
434
277
|
) -> RunGeneratorResult:
|
|
435
278
|
"""Run generators that generate files or file parts."""
|
|
436
279
|
ret = RunGeneratorResult()
|
|
@@ -446,7 +289,7 @@ def run_file_generators(
|
|
|
446
289
|
else:
|
|
447
290
|
raise RuntimeError(f"Unknown generator class type: cls={gen.__class__} TYPE={gen.__class__.TYPE}")
|
|
448
291
|
try:
|
|
449
|
-
result = run_generator_fn(gen, device
|
|
292
|
+
result = run_generator_fn(gen, device)
|
|
450
293
|
except NotSupportedDevice as exc:
|
|
451
294
|
logger.info("generator %s raised unsupported error: %r", gen, exc)
|
|
452
295
|
continue
|
|
@@ -457,7 +300,7 @@ def run_file_generators(
|
|
|
457
300
|
|
|
458
301
|
|
|
459
302
|
@tracing.function(min_duration="0.5")
|
|
460
|
-
def _run_entire_generator(gen: "Entire", device: "Device"
|
|
303
|
+
def _run_entire_generator(gen: "Entire", device: "Device") -> Optional[GeneratorResult]:
|
|
461
304
|
logger = get_logger(generator=_make_generator_ctx(gen))
|
|
462
305
|
if not gen.supports_device(device):
|
|
463
306
|
logger.info("generator %s is not supported for device %s", gen, device.hostname)
|
|
@@ -473,7 +316,7 @@ def _run_entire_generator(gen: "Entire", device: "Device", storage: Storage) ->
|
|
|
473
316
|
raise RuntimeError("entire generator should return non-empty path")
|
|
474
317
|
|
|
475
318
|
logger.info("Generating ENTIRE ...")
|
|
476
|
-
with GeneratorPerfMesurer(gen,
|
|
319
|
+
with GeneratorPerfMesurer(gen, trace_min_duration="0.5") as pm:
|
|
477
320
|
output = gen(device)
|
|
478
321
|
|
|
479
322
|
return GeneratorEntireResult(
|
|
@@ -495,7 +338,6 @@ def _make_generator_ctx(gen):
|
|
|
495
338
|
def _run_json_fragment_generator(
|
|
496
339
|
gen: "JSONFragment",
|
|
497
340
|
device: "Device",
|
|
498
|
-
storage: Storage,
|
|
499
341
|
) -> Optional[GeneratorResult]:
|
|
500
342
|
logger = get_logger(generator=_make_generator_ctx(gen))
|
|
501
343
|
if not gen.supports_device(device):
|
|
@@ -520,7 +362,7 @@ def _run_json_fragment_generator(
|
|
|
520
362
|
acl_safe = [safe_acl_item_or_list_of_items]
|
|
521
363
|
|
|
522
364
|
logger.info("Generating JSON_FRAGMENT ...")
|
|
523
|
-
with GeneratorPerfMesurer(gen
|
|
365
|
+
with GeneratorPerfMesurer(gen) as pm:
|
|
524
366
|
config = gen(device)
|
|
525
367
|
reload_cmds = gen.get_reload_cmds(device)
|
|
526
368
|
return GeneratorJSONFragmentResult(
|
|
@@ -559,7 +401,7 @@ def _get_generators(module_paths: Union[List[str], dict], storage, device=None):
|
|
|
559
401
|
return res_generators
|
|
560
402
|
|
|
561
403
|
|
|
562
|
-
def _get_ref_generators(module_paths: List[str], storage):
|
|
404
|
+
def _get_ref_generators(module_paths: List[str], storage, device):
|
|
563
405
|
if isinstance(module_paths, dict):
|
|
564
406
|
module_paths = module_paths.get("default")
|
|
565
407
|
res_generators = []
|
|
@@ -570,302 +412,6 @@ def _get_ref_generators(module_paths: List[str], storage):
|
|
|
570
412
|
return res_generators
|
|
571
413
|
|
|
572
414
|
|
|
573
|
-
class InvalidValueFromGenerator(ValueError):
|
|
574
|
-
pass
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
class GenStringable(abc.ABC):
|
|
578
|
-
@abc.abstractmethod
|
|
579
|
-
def gen_str(self) -> str:
|
|
580
|
-
pass
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
ParamsList = tabparser.JuniperList
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
def _filter_str(value: Union[str, int, float, tabparser.JuniperList, ParamsList, GenStringable]):
|
|
587
|
-
if isinstance(value, (
|
|
588
|
-
str,
|
|
589
|
-
int,
|
|
590
|
-
float,
|
|
591
|
-
tabparser.JuniperList,
|
|
592
|
-
ParamsList,
|
|
593
|
-
)):
|
|
594
|
-
return str(value)
|
|
595
|
-
|
|
596
|
-
if hasattr(value, "gen_str") and callable(value.gen_str):
|
|
597
|
-
return value.gen_str()
|
|
598
|
-
|
|
599
|
-
raise InvalidValueFromGenerator("Invalid yield type: %s(%s)" % (type(value).__name__, value))
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
# =====
|
|
603
|
-
class BaseGenerator:
|
|
604
|
-
TYPE: str
|
|
605
|
-
TAGS: list[str]
|
|
606
|
-
|
|
607
|
-
def supports_device(self, device: Device) -> bool: # pylint: disable=unused-argument
|
|
608
|
-
return True
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
class TreeGenerator(BaseGenerator):
|
|
612
|
-
def __init__(self, indent=" "):
|
|
613
|
-
self._indents = []
|
|
614
|
-
self._rows = []
|
|
615
|
-
self._block_path = []
|
|
616
|
-
self._indent = indent
|
|
617
|
-
|
|
618
|
-
@tracing.contextmanager(min_duration="0.1")
|
|
619
|
-
@contextlib.contextmanager
|
|
620
|
-
def block(self, *tokens, indent=None):
|
|
621
|
-
span = tracing_connector.get().get_current_span()
|
|
622
|
-
if span:
|
|
623
|
-
span.set_attribute("tokens", " ".join(map(str, tokens)))
|
|
624
|
-
|
|
625
|
-
indent = self._indent if indent is None else indent
|
|
626
|
-
block = " ".join(map(_filter_str, tokens))
|
|
627
|
-
self._block_path.append(block)
|
|
628
|
-
self._append_text(block)
|
|
629
|
-
self._indents.append(indent)
|
|
630
|
-
yield
|
|
631
|
-
self._indents.pop(-1)
|
|
632
|
-
self._block_path.pop(-1)
|
|
633
|
-
|
|
634
|
-
@contextlib.contextmanager
|
|
635
|
-
def block_if(self, *tokens, condition=DefaultBlockIfCondition):
|
|
636
|
-
if condition is DefaultBlockIfCondition:
|
|
637
|
-
condition = (None not in tokens and "" not in tokens)
|
|
638
|
-
if condition:
|
|
639
|
-
with self.block(*tokens):
|
|
640
|
-
yield
|
|
641
|
-
return
|
|
642
|
-
yield
|
|
643
|
-
|
|
644
|
-
@contextlib.contextmanager
|
|
645
|
-
def multiblock(self, *blocks):
|
|
646
|
-
if blocks:
|
|
647
|
-
blk = blocks[0]
|
|
648
|
-
tokens = blk if isinstance(blk, (list, tuple)) else [blk]
|
|
649
|
-
with self.block(*tokens):
|
|
650
|
-
with self.multiblock(*blocks[1:]):
|
|
651
|
-
yield
|
|
652
|
-
return
|
|
653
|
-
yield
|
|
654
|
-
|
|
655
|
-
@contextlib.contextmanager
|
|
656
|
-
def multiblock_if(self, *blocks, condition=DefaultBlockIfCondition):
|
|
657
|
-
if condition is DefaultBlockIfCondition:
|
|
658
|
-
condition = (None not in blocks)
|
|
659
|
-
if condition:
|
|
660
|
-
if blocks:
|
|
661
|
-
blk = blocks[0]
|
|
662
|
-
tokens = blk if isinstance(blk, (list, tuple)) else [blk]
|
|
663
|
-
with self.block(*tokens):
|
|
664
|
-
with self.multiblock(*blocks[1:]):
|
|
665
|
-
yield
|
|
666
|
-
return
|
|
667
|
-
yield
|
|
668
|
-
|
|
669
|
-
# ===
|
|
670
|
-
def _append_text(self, text):
|
|
671
|
-
self._append_text_cb(text)
|
|
672
|
-
|
|
673
|
-
def _append_text_cb(self, text, row_cb=None):
|
|
674
|
-
for row in _split_and_strip(text):
|
|
675
|
-
if row_cb:
|
|
676
|
-
row = row_cb(row)
|
|
677
|
-
self._rows.append("".join(self._indents) + row)
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
class TextGenerator(TreeGenerator):
|
|
681
|
-
def __add__(self, line):
|
|
682
|
-
self._append_text(line)
|
|
683
|
-
return self
|
|
684
|
-
|
|
685
|
-
def __iter__(self):
|
|
686
|
-
yield from self._rows
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
class PartialGenerator(TreeGenerator):
|
|
690
|
-
TYPE = "PARTIAL"
|
|
691
|
-
TAGS: List[str] = []
|
|
692
|
-
|
|
693
|
-
def __init__(self, storage):
|
|
694
|
-
super().__init__()
|
|
695
|
-
self.storage = storage
|
|
696
|
-
self._annotate = False
|
|
697
|
-
self._running_gen = None
|
|
698
|
-
self._annotations = []
|
|
699
|
-
self._annotation_module = self.__class__.__module__ or ""
|
|
700
|
-
|
|
701
|
-
def supports_device(self, device: Device) -> bool:
|
|
702
|
-
if self.__class__.run is PartialGenerator.run:
|
|
703
|
-
return bool(self._get_vendor_func(device.hw.vendor, "run"))
|
|
704
|
-
else:
|
|
705
|
-
return True
|
|
706
|
-
|
|
707
|
-
def acl(self, device):
|
|
708
|
-
acl_func = self._get_vendor_func(device.hw.vendor, "acl")
|
|
709
|
-
if acl_func:
|
|
710
|
-
return acl_func(device)
|
|
711
|
-
|
|
712
|
-
def acl_safe(self, device):
|
|
713
|
-
acl_func = self._get_vendor_func(device.hw.vendor, "acl_safe")
|
|
714
|
-
if acl_func:
|
|
715
|
-
return acl_func(device)
|
|
716
|
-
|
|
717
|
-
def run(self, device) -> Iterable[Union[str, tuple]]:
|
|
718
|
-
run_func = self._get_vendor_func(device.hw.vendor, "run")
|
|
719
|
-
if run_func:
|
|
720
|
-
return run_func(device)
|
|
721
|
-
|
|
722
|
-
def get_user_runner(self, device):
|
|
723
|
-
if self.__class__.run is not PartialGenerator.run:
|
|
724
|
-
return self.run
|
|
725
|
-
return self._get_vendor_func(device.hw.vendor, "run")
|
|
726
|
-
|
|
727
|
-
def _get_vendor_func(self, vendor: str, func_name: str):
|
|
728
|
-
attr_name = f"{func_name}_{vendor}"
|
|
729
|
-
if hasattr(self, attr_name):
|
|
730
|
-
return getattr(self, attr_name)
|
|
731
|
-
return None
|
|
732
|
-
|
|
733
|
-
# =====
|
|
734
|
-
|
|
735
|
-
@classmethod
|
|
736
|
-
def get_aliases(cls) -> Set[str]:
|
|
737
|
-
return {cls.__name__, *cls.TAGS}
|
|
738
|
-
|
|
739
|
-
def __call__(self, device, annotate=False):
|
|
740
|
-
self._indents = []
|
|
741
|
-
self._rows = []
|
|
742
|
-
self._running_gen = self.run(device)
|
|
743
|
-
self._annotate = annotate
|
|
744
|
-
|
|
745
|
-
if annotate and self.__class__.__module__:
|
|
746
|
-
self._annotation_module = ".".join(self.__class__.__module__.split(".")[-2:])
|
|
747
|
-
|
|
748
|
-
for text in self._running_gen:
|
|
749
|
-
if isinstance(text, tuple):
|
|
750
|
-
text = " ".join(map(_filter_str, flatten(text)))
|
|
751
|
-
else:
|
|
752
|
-
text = _filter_str(text)
|
|
753
|
-
self._append_text(text)
|
|
754
|
-
|
|
755
|
-
for row in self._rows:
|
|
756
|
-
assert re.search(r"\bNone\b", row) is None, "Found 'None' in yield result: %s" % (row)
|
|
757
|
-
if annotate:
|
|
758
|
-
generated_rows = (add_annotation(x, y) for (x, y) in zip(self._rows, self._annotations))
|
|
759
|
-
else:
|
|
760
|
-
generated_rows = self._rows
|
|
761
|
-
return "\n".join(generated_rows) + "\n"
|
|
762
|
-
|
|
763
|
-
def _append_text(self, text):
|
|
764
|
-
def annotation_cb(row):
|
|
765
|
-
annotation = "%s:%d" % self.get_running_line()
|
|
766
|
-
self._annotations.append(annotation)
|
|
767
|
-
return row
|
|
768
|
-
|
|
769
|
-
self._append_text_cb(
|
|
770
|
-
text,
|
|
771
|
-
annotation_cb if self._annotate else None
|
|
772
|
-
)
|
|
773
|
-
|
|
774
|
-
def get_running_line(self):
|
|
775
|
-
if not self._running_gen or not self._running_gen.gi_frame:
|
|
776
|
-
return (repr(self._running_gen), -1)
|
|
777
|
-
return self._annotation_module, self._running_gen.gi_frame.f_lineno
|
|
778
|
-
|
|
779
|
-
@classmethod
|
|
780
|
-
def literal(cls, item):
|
|
781
|
-
return '"{}"'.format(item)
|
|
782
|
-
|
|
783
|
-
def __repr__(self):
|
|
784
|
-
return "<%s>" % self.__class__.__name__
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
class RefGenerator(PartialGenerator):
|
|
788
|
-
def __init__(self, storage, groups=None):
|
|
789
|
-
super().__init__(storage)
|
|
790
|
-
self.groups = groups
|
|
791
|
-
|
|
792
|
-
def ref(self, device):
|
|
793
|
-
if hasattr(self, "ref_" + device.hw.vendor):
|
|
794
|
-
return getattr(self, "ref_" + device.hw.vendor)(device)
|
|
795
|
-
return ""
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
class Entire(BaseGenerator):
|
|
799
|
-
TYPE = "ENTIRE"
|
|
800
|
-
TAGS: List[str] = []
|
|
801
|
-
REQUIRED_PACKAGES: FrozenSet[str] = frozenset()
|
|
802
|
-
|
|
803
|
-
def __init__(self, storage):
|
|
804
|
-
self.storage = storage
|
|
805
|
-
# между генераторами для одного и того же path - выбирается тот что больше
|
|
806
|
-
if not hasattr(self, "prio"):
|
|
807
|
-
self.prio = 100
|
|
808
|
-
self.__device = None
|
|
809
|
-
|
|
810
|
-
def supports_device(self, device: Device):
|
|
811
|
-
return bool(self.path(device))
|
|
812
|
-
|
|
813
|
-
def run(self, device) -> Union[None, str, Iterable[Union[str, tuple]]]:
|
|
814
|
-
raise NotImplementedError
|
|
815
|
-
|
|
816
|
-
def reload(self, device) -> Optional[str]: # pylint: disable=unused-argument
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
def get_reload_cmds(self, device) -> str:
|
|
820
|
-
ret = self.reload(device) or ""
|
|
821
|
-
path = self.path(device)
|
|
822
|
-
if path and device.hw.PC and device.hw.soft.startswith(("Cumulus", "SwitchDev", "SONiC")):
|
|
823
|
-
parts = []
|
|
824
|
-
if ret:
|
|
825
|
-
parts.append(ret)
|
|
826
|
-
parts.append("/usr/bin/etckeeper commitreload %s" % path)
|
|
827
|
-
return "\n".join(parts)
|
|
828
|
-
return ret
|
|
829
|
-
|
|
830
|
-
def path(self, device) -> Optional[str]:
|
|
831
|
-
raise NotImplementedError("Required PATH for ENTIRE generator")
|
|
832
|
-
|
|
833
|
-
# pylint: disable=unused-argument
|
|
834
|
-
def is_safe(self, device) -> bool:
|
|
835
|
-
"""Output gen results when --acl-safe flag is used"""
|
|
836
|
-
return False
|
|
837
|
-
|
|
838
|
-
def read(self, path) -> str:
|
|
839
|
-
return pkgutil.get_data(__name__, path).decode()
|
|
840
|
-
|
|
841
|
-
def mako(self, text, **kwargs) -> str:
|
|
842
|
-
return mako_render(text, dedent=True, device=self.__device, **kwargs)
|
|
843
|
-
|
|
844
|
-
def jinja(self, text, **kwargs) -> str:
|
|
845
|
-
return jinja_render(text, dedent=True, device=self.__device, **kwargs)
|
|
846
|
-
|
|
847
|
-
# =====
|
|
848
|
-
|
|
849
|
-
@classmethod
|
|
850
|
-
def get_aliases(cls) -> Set[str]:
|
|
851
|
-
return {cls.__name__, *cls.TAGS}
|
|
852
|
-
|
|
853
|
-
def __call__(self, device):
|
|
854
|
-
self.__device = device
|
|
855
|
-
parts = []
|
|
856
|
-
run_res = self.run(device)
|
|
857
|
-
if isinstance(run_res, str):
|
|
858
|
-
run_res = (run_res,)
|
|
859
|
-
if run_res is None or not isinstance(run_res, (tuple, types.GeneratorType)):
|
|
860
|
-
raise Exception("generator %s returns %s" % (self.__class__.__name__, type(run_res)))
|
|
861
|
-
for text in run_res:
|
|
862
|
-
if isinstance(text, tuple):
|
|
863
|
-
text = " ".join(map(_filter_str, flatten(text)))
|
|
864
|
-
assert re.search(r"\bNone\b", text) is None, "Found 'None' in yield result: %s" % text
|
|
865
|
-
parts.append(text)
|
|
866
|
-
return "\n".join(parts)
|
|
867
|
-
|
|
868
|
-
|
|
869
415
|
def select_generators(gens: GenSelectOptions, classes: Iterable[BaseGenerator]):
|
|
870
416
|
def contains(obj, where):
|
|
871
417
|
if where:
|
|
@@ -887,135 +433,3 @@ def select_generators(gens: GenSelectOptions, classes: Iterable[BaseGenerator]):
|
|
|
887
433
|
flts.append(lambda c: not contains(c, gens.excluded_gens))
|
|
888
434
|
|
|
889
435
|
return filter(lambda x: all(f(x) for f in flts), classes)
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
def _split_and_strip(text):
|
|
893
|
-
if "\n" in text:
|
|
894
|
-
rows = textwrap.dedent(text).strip().split("\n")
|
|
895
|
-
else:
|
|
896
|
-
rows = [text]
|
|
897
|
-
return rows
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
def _combine_acl_text(
|
|
901
|
-
partial_results: Dict[str, GeneratorPartialResult],
|
|
902
|
-
acl_getter: Callable[[GeneratorPartialResult], str]
|
|
903
|
-
) -> str:
|
|
904
|
-
acl_text = ""
|
|
905
|
-
for gr in partial_results.values():
|
|
906
|
-
for line in textwrap.dedent(acl_getter(gr)).split("\n"):
|
|
907
|
-
if line and not line.isspace():
|
|
908
|
-
acl_text += line.rstrip()
|
|
909
|
-
acl_text += fr" %generator_names={gr.name}"
|
|
910
|
-
acl_text += "\n"
|
|
911
|
-
return acl_text
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
class JSONFragment(TreeGenerator):
|
|
915
|
-
"""Generates parts of JSON config file."""
|
|
916
|
-
|
|
917
|
-
TYPE = "JSON_FRAGMENT"
|
|
918
|
-
TAGS: List[str] = []
|
|
919
|
-
|
|
920
|
-
def __init__(self, storage: Storage):
|
|
921
|
-
super().__init__()
|
|
922
|
-
self.storage = storage
|
|
923
|
-
self._json_config: Dict[str, Any] = {}
|
|
924
|
-
self._config_pointer: List[str] = []
|
|
925
|
-
|
|
926
|
-
# if two generators edit same file, commands from generator with greater `reload_prio` will be used
|
|
927
|
-
if not hasattr(self, "reload_prio"):
|
|
928
|
-
self.reload_prio = 100
|
|
929
|
-
|
|
930
|
-
def supports_device(self, device: Device):
|
|
931
|
-
return bool(self.path(device))
|
|
932
|
-
|
|
933
|
-
def path(self, device: Device) -> Optional[str]:
|
|
934
|
-
raise NotImplementedError("Required PATH for JSON_FRAGMENT generator")
|
|
935
|
-
|
|
936
|
-
@classmethod
|
|
937
|
-
def get_aliases(cls) -> Set[str]:
|
|
938
|
-
return {cls.__name__, *cls.TAGS}
|
|
939
|
-
|
|
940
|
-
def acl(self, device: Device) -> Union[str, List[str]]:
|
|
941
|
-
"""
|
|
942
|
-
Restrict the generator to a specified ACL using JSON Pointer syntax.
|
|
943
|
-
|
|
944
|
-
Expected ACL to be a list of strings, but a single string is also allowed.
|
|
945
|
-
"""
|
|
946
|
-
raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
|
|
947
|
-
|
|
948
|
-
def acl_safe(self, device: Device) -> Union[str, List[str]]:
|
|
949
|
-
"""
|
|
950
|
-
Restrict the generator to a specified safe ACL using JSON Pointer syntax.
|
|
951
|
-
|
|
952
|
-
Expected ACL to be a list of strings, but a single string is also allowed.
|
|
953
|
-
"""
|
|
954
|
-
raise NotImplementedError("Required ACL for JSON_FRAGMENT generator")
|
|
955
|
-
|
|
956
|
-
def run(self, device: Device):
|
|
957
|
-
raise NotImplementedError
|
|
958
|
-
|
|
959
|
-
def get_reload_cmds(self, device: Device) -> str:
|
|
960
|
-
ret = self.reload(device) or ""
|
|
961
|
-
return ret
|
|
962
|
-
|
|
963
|
-
def reload(self, device) -> Optional[str]:
|
|
964
|
-
raise NotImplementedError
|
|
965
|
-
|
|
966
|
-
@contextlib.contextmanager
|
|
967
|
-
def block(self, *tokens, indent=None): # pylint: disable=unused-argument
|
|
968
|
-
block_str = "".join(map(_filter_str, tokens))
|
|
969
|
-
self._config_pointer.append(block_str)
|
|
970
|
-
try:
|
|
971
|
-
yield
|
|
972
|
-
finally:
|
|
973
|
-
self._config_pointer.pop()
|
|
974
|
-
|
|
975
|
-
@contextlib.contextmanager
|
|
976
|
-
def block_piped(self, *tokens, indent=None): # pylint: disable=unused-argument
|
|
977
|
-
block_str = "|".join(map(_filter_str, tokens))
|
|
978
|
-
self._config_pointer.append(block_str)
|
|
979
|
-
try:
|
|
980
|
-
yield
|
|
981
|
-
finally:
|
|
982
|
-
self._config_pointer.pop()
|
|
983
|
-
|
|
984
|
-
def __call__(self, device: Device, annotate: bool = False):
|
|
985
|
-
for cfg_fragment in self.run(device):
|
|
986
|
-
self._set_or_replace_dict(self._config_pointer, cfg_fragment)
|
|
987
|
-
return self._json_config
|
|
988
|
-
|
|
989
|
-
def _set_or_replace_dict(self, pointer, value):
|
|
990
|
-
if not pointer:
|
|
991
|
-
if self._json_config == {}:
|
|
992
|
-
self._json_config = value
|
|
993
|
-
else:
|
|
994
|
-
self._json_config = [self._json_config, value]
|
|
995
|
-
else:
|
|
996
|
-
self._set_dict(self._json_config, pointer, value)
|
|
997
|
-
|
|
998
|
-
@classmethod
|
|
999
|
-
def _to_str(cls, value: Any) -> str:
|
|
1000
|
-
if isinstance(value, str):
|
|
1001
|
-
return value
|
|
1002
|
-
elif isinstance(value, list):
|
|
1003
|
-
return [cls._to_str(x) for x in value]
|
|
1004
|
-
elif isinstance(value, dict):
|
|
1005
|
-
for k, v in value.items():
|
|
1006
|
-
value[k] = cls._to_str(v)
|
|
1007
|
-
return value
|
|
1008
|
-
return str(value)
|
|
1009
|
-
|
|
1010
|
-
@classmethod
|
|
1011
|
-
def _set_dict(cls, cfg, pointer, value):
|
|
1012
|
-
# pointer has at least one key
|
|
1013
|
-
if len(pointer) == 1:
|
|
1014
|
-
if pointer[0] in cfg:
|
|
1015
|
-
cfg[pointer[0]] = [cfg[pointer[0]], cls._to_str(value)]
|
|
1016
|
-
else:
|
|
1017
|
-
cfg[pointer[0]] = cls._to_str(value)
|
|
1018
|
-
else:
|
|
1019
|
-
if pointer[0] not in cfg:
|
|
1020
|
-
cfg[pointer[0]] = {}
|
|
1021
|
-
cls._set_dict(cfg[pointer[0]], pointer[1:], cls._to_str(value))
|