annet 0.0__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 (137) hide show
  1. annet/__init__.py +61 -0
  2. annet/adapters/__init__.py +0 -0
  3. annet/adapters/netbox/__init__.py +0 -0
  4. annet/adapters/netbox/common/__init__.py +0 -0
  5. annet/adapters/netbox/common/client.py +87 -0
  6. annet/adapters/netbox/common/manufacturer.py +62 -0
  7. annet/adapters/netbox/common/models.py +105 -0
  8. annet/adapters/netbox/common/query.py +23 -0
  9. annet/adapters/netbox/common/status_client.py +25 -0
  10. annet/adapters/netbox/common/storage_opts.py +14 -0
  11. annet/adapters/netbox/provider.py +34 -0
  12. annet/adapters/netbox/v24/__init__.py +0 -0
  13. annet/adapters/netbox/v24/api_models.py +73 -0
  14. annet/adapters/netbox/v24/client.py +59 -0
  15. annet/adapters/netbox/v24/storage.py +196 -0
  16. annet/adapters/netbox/v37/__init__.py +0 -0
  17. annet/adapters/netbox/v37/api_models.py +38 -0
  18. annet/adapters/netbox/v37/client.py +62 -0
  19. annet/adapters/netbox/v37/storage.py +149 -0
  20. annet/annet.py +25 -0
  21. annet/annlib/__init__.py +7 -0
  22. annet/annlib/command.py +49 -0
  23. annet/annlib/diff.py +158 -0
  24. annet/annlib/errors.py +8 -0
  25. annet/annlib/filter_acl.py +196 -0
  26. annet/annlib/jsontools.py +116 -0
  27. annet/annlib/lib.py +495 -0
  28. annet/annlib/netdev/__init__.py +0 -0
  29. annet/annlib/netdev/db.py +62 -0
  30. annet/annlib/netdev/devdb/__init__.py +28 -0
  31. annet/annlib/netdev/devdb/data/devdb.json +137 -0
  32. annet/annlib/netdev/views/__init__.py +0 -0
  33. annet/annlib/netdev/views/dump.py +121 -0
  34. annet/annlib/netdev/views/hardware.py +112 -0
  35. annet/annlib/output.py +246 -0
  36. annet/annlib/patching.py +533 -0
  37. annet/annlib/rbparser/__init__.py +0 -0
  38. annet/annlib/rbparser/acl.py +120 -0
  39. annet/annlib/rbparser/deploying.py +55 -0
  40. annet/annlib/rbparser/ordering.py +52 -0
  41. annet/annlib/rbparser/platform.py +51 -0
  42. annet/annlib/rbparser/syntax.py +115 -0
  43. annet/annlib/rulebook/__init__.py +0 -0
  44. annet/annlib/rulebook/common.py +350 -0
  45. annet/annlib/tabparser.py +648 -0
  46. annet/annlib/types.py +35 -0
  47. annet/api/__init__.py +826 -0
  48. annet/argparse.py +415 -0
  49. annet/cli.py +237 -0
  50. annet/cli_args.py +503 -0
  51. annet/configs/context.yml +18 -0
  52. annet/configs/logging.yaml +39 -0
  53. annet/connectors.py +77 -0
  54. annet/deploy.py +536 -0
  55. annet/diff.py +84 -0
  56. annet/executor.py +551 -0
  57. annet/filtering.py +40 -0
  58. annet/gen.py +865 -0
  59. annet/generators/__init__.py +435 -0
  60. annet/generators/base.py +136 -0
  61. annet/generators/common/__init__.py +0 -0
  62. annet/generators/common/initial.py +33 -0
  63. annet/generators/entire.py +97 -0
  64. annet/generators/exceptions.py +10 -0
  65. annet/generators/jsonfragment.py +125 -0
  66. annet/generators/partial.py +119 -0
  67. annet/generators/perf.py +79 -0
  68. annet/generators/ref.py +15 -0
  69. annet/generators/result.py +127 -0
  70. annet/hardware.py +45 -0
  71. annet/implicit.py +139 -0
  72. annet/lib.py +128 -0
  73. annet/output.py +167 -0
  74. annet/parallel.py +448 -0
  75. annet/patching.py +25 -0
  76. annet/reference.py +148 -0
  77. annet/rulebook/__init__.py +114 -0
  78. annet/rulebook/arista/__init__.py +0 -0
  79. annet/rulebook/arista/iface.py +16 -0
  80. annet/rulebook/aruba/__init__.py +16 -0
  81. annet/rulebook/aruba/ap_env.py +146 -0
  82. annet/rulebook/aruba/misc.py +8 -0
  83. annet/rulebook/cisco/__init__.py +0 -0
  84. annet/rulebook/cisco/iface.py +68 -0
  85. annet/rulebook/cisco/misc.py +57 -0
  86. annet/rulebook/cisco/vlandb.py +90 -0
  87. annet/rulebook/common.py +19 -0
  88. annet/rulebook/deploying.py +87 -0
  89. annet/rulebook/huawei/__init__.py +0 -0
  90. annet/rulebook/huawei/aaa.py +75 -0
  91. annet/rulebook/huawei/bgp.py +97 -0
  92. annet/rulebook/huawei/iface.py +33 -0
  93. annet/rulebook/huawei/misc.py +337 -0
  94. annet/rulebook/huawei/vlandb.py +115 -0
  95. annet/rulebook/juniper/__init__.py +107 -0
  96. annet/rulebook/nexus/__init__.py +0 -0
  97. annet/rulebook/nexus/iface.py +92 -0
  98. annet/rulebook/patching.py +143 -0
  99. annet/rulebook/ribbon/__init__.py +12 -0
  100. annet/rulebook/texts/arista.deploy +20 -0
  101. annet/rulebook/texts/arista.order +125 -0
  102. annet/rulebook/texts/arista.rul +59 -0
  103. annet/rulebook/texts/aruba.deploy +20 -0
  104. annet/rulebook/texts/aruba.order +83 -0
  105. annet/rulebook/texts/aruba.rul +87 -0
  106. annet/rulebook/texts/cisco.deploy +27 -0
  107. annet/rulebook/texts/cisco.order +82 -0
  108. annet/rulebook/texts/cisco.rul +105 -0
  109. annet/rulebook/texts/huawei.deploy +188 -0
  110. annet/rulebook/texts/huawei.order +388 -0
  111. annet/rulebook/texts/huawei.rul +471 -0
  112. annet/rulebook/texts/juniper.rul +120 -0
  113. annet/rulebook/texts/nexus.deploy +24 -0
  114. annet/rulebook/texts/nexus.order +85 -0
  115. annet/rulebook/texts/nexus.rul +83 -0
  116. annet/rulebook/texts/nokia.rul +31 -0
  117. annet/rulebook/texts/pc.order +5 -0
  118. annet/rulebook/texts/pc.rul +9 -0
  119. annet/rulebook/texts/ribbon.deploy +22 -0
  120. annet/rulebook/texts/ribbon.rul +77 -0
  121. annet/rulebook/texts/routeros.order +38 -0
  122. annet/rulebook/texts/routeros.rul +45 -0
  123. annet/storage.py +125 -0
  124. annet/tabparser.py +36 -0
  125. annet/text_term_format.py +95 -0
  126. annet/tracing.py +170 -0
  127. annet/types.py +227 -0
  128. annet-0.0.dist-info/AUTHORS +21 -0
  129. annet-0.0.dist-info/LICENSE +21 -0
  130. annet-0.0.dist-info/METADATA +26 -0
  131. annet-0.0.dist-info/RECORD +137 -0
  132. annet-0.0.dist-info/WHEEL +5 -0
  133. annet-0.0.dist-info/entry_points.txt +5 -0
  134. annet-0.0.dist-info/top_level.txt +2 -0
  135. annet_generators/__init__.py +0 -0
  136. annet_generators/example/__init__.py +12 -0
  137. annet_generators/example/lldp.py +53 -0
@@ -0,0 +1,435 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import importlib
5
+ import os
6
+ import textwrap
7
+ from collections import OrderedDict as odict
8
+ from typing import (
9
+ FrozenSet,
10
+ Iterable,
11
+ List,
12
+ Optional,
13
+ Union,
14
+ )
15
+
16
+ from annet.annlib.rbparser.acl import compile_acl_text
17
+ from contextlog import get_logger
18
+
19
+ from annet.storage import Device
20
+
21
+ from annet import patching, tabparser, tracing
22
+ from annet.cli_args import GenSelectOptions, ShowGeneratorsOptions
23
+ from annet.lib import (
24
+ get_context,
25
+ )
26
+ from annet.tracing import tracing_connector
27
+ from annet.types import (
28
+ GeneratorEntireResult,
29
+ GeneratorJSONFragmentResult,
30
+ GeneratorPartialResult,
31
+ GeneratorPartialRunArgs,
32
+ GeneratorResult,
33
+ )
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
46
+
47
+ # =====
48
+ DISABLED_TAG = "disable"
49
+
50
+
51
+ # =====
52
+ def get_list(args: ShowGeneratorsOptions):
53
+ if args.generators_context is not None:
54
+ os.environ["ANN_GENERATORS_CONTEXT"] = args.generators_context
55
+ return {
56
+ cls.__class__.__name__: {
57
+ "type": cls.TYPE,
58
+ "tags": set(cls.TAGS),
59
+ "description": get_description(cls.__class__),
60
+ }
61
+ for cls in _get_generators(get_context()["generators"], None)
62
+ }
63
+
64
+
65
+ def get_description(gen_cls) -> str:
66
+ return textwrap.dedent(" ".join([
67
+ (gen_cls.__doc__ or ""),
68
+ ("Disabled. Use '-g %s' to enable" % gen_cls.__name__ if DISABLED_TAG in gen_cls.TAGS else "")
69
+ ])).strip()
70
+
71
+
72
+ def validate_genselect(gens: GenSelectOptions, all_classes):
73
+ logger = get_logger()
74
+ unknown_err = "Unknown generator alias %s"
75
+ all_aliases = {
76
+ alias
77
+ for cls in all_classes
78
+ for alias in cls.get_aliases()
79
+ }
80
+ for gen_set in (gens.allowed_gens, gens.force_enabled):
81
+ for alias in set(gen_set or ()) - all_aliases:
82
+ logger.error(unknown_err, alias)
83
+ raise Exception(unknown_err % alias)
84
+
85
+
86
+ @dataclasses.dataclass
87
+ class Generators:
88
+ """Collection of various types of generators."""
89
+
90
+ partial: List[PartialGenerator] = dataclasses.field(default_factory=list)
91
+ entire: List[Entire] = dataclasses.field(default_factory=list)
92
+ ref: List[RefGenerator] = dataclasses.field(default_factory=list)
93
+ json_fragment: List[JSONFragment] = dataclasses.field(default_factory=list)
94
+
95
+
96
+ def build_generators(storage, gens: GenSelectOptions, device: Optional[Device] = None) -> Generators:
97
+ """Return generators that meet the gens filter conditions."""
98
+ if gens.generators_context is not None:
99
+ os.environ["ANN_GENERATORS_CONTEXT"] = gens.generators_context
100
+ all_generators = _get_generators(get_context()["generators"], storage, device)
101
+ ref_generators = _get_ref_generators(get_context()["generators"], storage, device)
102
+ validate_genselect(gens, all_generators)
103
+ classes = list(select_generators(gens, all_generators))
104
+ partial = [obj for obj in classes if obj.TYPE == "PARTIAL"]
105
+ entire = [obj for obj in classes if obj.TYPE == "ENTIRE"]
106
+ entire = list(sorted(entire, key=lambda x: x.prio, reverse=True))
107
+ json_fragment = [obj for obj in classes if obj.TYPE == "JSON_FRAGMENT"]
108
+ return Generators(
109
+ partial=partial,
110
+ entire=entire,
111
+ json_fragment=json_fragment,
112
+ ref=ref_generators,
113
+ )
114
+
115
+
116
+ @tracing.function
117
+ def run_partial_initial(device):
118
+ from .common.initial import InitialConfig
119
+
120
+ tracing_connector.get().set_device_attributes(tracing_connector.get().get_current_span(), device)
121
+
122
+ run_args = GeneratorPartialRunArgs(device)
123
+ return run_partial_generators([InitialConfig(storage=device.storage)], [], run_args)
124
+
125
+
126
+ @tracing.function
127
+ def run_partial_generators(
128
+ gens: List["PartialGenerator"],
129
+ ref_gens: List["RefGenerator"],
130
+ run_args: GeneratorPartialRunArgs,
131
+ ):
132
+ logger = get_logger(host=run_args.device.hostname)
133
+ tracing_connector.get().set_device_attributes(tracing_connector.get().get_current_span(), run_args.device)
134
+
135
+ ret = RunGeneratorResult()
136
+ if run_args.generators_context is not None:
137
+ os.environ["ANN_GENERATORS_CONTEXT"] = run_args.generators_context
138
+
139
+ for gen in ref_gens:
140
+ ret.ref_matcher.add(gen.ref(run_args.device), gen)
141
+
142
+ logger.debug("Generating selected PARTIALs ...")
143
+
144
+ for gen in gens:
145
+ try:
146
+ result = _run_partial_generator(gen, run_args)
147
+ except NotSupportedDevice as exc:
148
+ logger.info("generator %s raised unsupported error: %r", gen, exc)
149
+ continue
150
+
151
+ if not result:
152
+ continue
153
+
154
+ config = result.safe_config if run_args.use_acl_safe else result.config
155
+
156
+ ref_match = ret.ref_matcher.match(config)
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__)
160
+
161
+ ret.ref_track.config(gen.__class__, config)
162
+ ret.add_partial(result)
163
+
164
+ return ret
165
+
166
+
167
+ @tracing.function(name="run_partial_generator")
168
+ def _run_partial_generator(gen: "PartialGenerator", run_args: GeneratorPartialRunArgs) -> Optional[GeneratorPartialResult]:
169
+ logger = get_logger(generator=_make_generator_ctx(gen))
170
+ device = run_args.device
171
+ output = ""
172
+ config = odict()
173
+ safe_config = odict()
174
+
175
+ if not gen.supports_device(device):
176
+ logger.info("generator %s is not supported for device %s", gen, device.hostname)
177
+ return None
178
+
179
+ span = tracing_connector.get().get_current_span()
180
+ if span:
181
+ tracing_connector.get().set_device_attributes(span, run_args.device)
182
+ tracing_connector.get().set_dimensions_attributes(span, gen, run_args.device)
183
+ span.set_attributes({
184
+ "use_acl": run_args.use_acl,
185
+ "use_acl_safe": run_args.use_acl_safe,
186
+ "generators_context": str(run_args.generators_context),
187
+ })
188
+
189
+ with GeneratorPerfMesurer(gen, run_args=run_args) as pm:
190
+ if not run_args.no_new:
191
+ if gen.get_user_runner(device):
192
+ logger.info("Generating PARTIAL ...")
193
+ try:
194
+ output = gen(device, run_args.annotate)
195
+ except NotSupportedDevice:
196
+ # это исключение нужно передать выше в оригинальном виде
197
+ raise
198
+ except Exception as err:
199
+ filename, lineno = gen.get_running_line()
200
+ logger.exception("Generator error in file '%s:%i'", filename, lineno)
201
+ raise GeneratorError(f"{gen} on {device}") from err
202
+
203
+ fmtr = tabparser.make_formatter(device.hw)
204
+ try:
205
+ config = tabparser.parse_to_tree(text=output, splitter=fmtr.split)
206
+ except tabparser.ParserError as err:
207
+ logger.exception("Parser error")
208
+ raise GeneratorError from err
209
+
210
+ acl = gen.acl(device) or ""
211
+ rules = compile_acl_text(textwrap.dedent(acl), device.hw.vendor)
212
+ acl_safe = gen.acl_safe(device) or ""
213
+ safe_rules = compile_acl_text(textwrap.dedent(acl_safe), device.hw.vendor)
214
+
215
+ if run_args.use_acl:
216
+ try:
217
+ with tracing_connector.get().start_as_current_span("apply_acl", tracer_name=__name__, min_duration="0.01") as acl_span:
218
+ tracing_connector.get().set_device_attributes(acl_span, run_args.device)
219
+ config = patching.apply_acl(
220
+ config=config,
221
+ rules=rules,
222
+ fatal_acl=True,
223
+ with_annotations=run_args.annotate,
224
+ )
225
+ if run_args.use_acl_safe:
226
+ with tracing_connector.get().start_as_current_span(
227
+ "apply_acl_safe",
228
+ tracer_name=__name__,
229
+ min_duration="0.01"
230
+ ) as acl_safe_span:
231
+ tracing_connector.get().set_device_attributes(acl_safe_span, run_args.device)
232
+ safe_config = patching.apply_acl(
233
+ config=config,
234
+ rules=safe_rules,
235
+ fatal_acl=False,
236
+ with_annotations=run_args.annotate,
237
+ )
238
+ except patching.AclError as err:
239
+ logger.error("ACL error: generator is not allowed to yield this command: %s", err)
240
+ raise GeneratorError from err
241
+ except NotImplementedError as err:
242
+ logger.error(str(err))
243
+ raise GeneratorError from err
244
+
245
+ return GeneratorPartialResult(
246
+ name=gen.__class__.__name__,
247
+ tags=gen.TAGS,
248
+ output=output,
249
+ acl=acl,
250
+ acl_rules=rules,
251
+ acl_safe=acl_safe,
252
+ acl_safe_rules=safe_rules,
253
+ config=config,
254
+ safe_config=safe_config,
255
+ perf=pm.last_result,
256
+ )
257
+
258
+
259
+ @tracing.function
260
+ def check_entire_generators_required_packages(gens, device_packages: FrozenSet[str]) -> List[str]:
261
+ errors: List[str] = []
262
+ for gen in gens:
263
+ if not gen.REQUIRED_PACKAGES.issubset(device_packages):
264
+ missing = gen.REQUIRED_PACKAGES - device_packages
265
+ missing_str = ", ".join("`{}'".format(pkg) for pkg in sorted(missing))
266
+ if len(missing) == 1:
267
+ errors.append("missing package {} required for {}".format(missing_str, gen))
268
+ else:
269
+ errors.append("missing packages {} required for {}".format(missing_str, gen))
270
+ return errors
271
+
272
+
273
+ @tracing.function
274
+ def run_file_generators(
275
+ gens: Iterable[Union["JSONFragment", "Entire"]],
276
+ device: "Device",
277
+ ) -> RunGeneratorResult:
278
+ """Run generators that generate files or file parts."""
279
+ ret = RunGeneratorResult()
280
+ logger = get_logger(host=device.hostname)
281
+ logger.debug("Generating selected ENTIREs and JSON_FRAGMENTs ...")
282
+ for gen in gens:
283
+ if gen.__class__.TYPE == "ENTIRE":
284
+ run_generator_fn = _run_entire_generator
285
+ add_result_fn = ret.add_entire
286
+ elif gen.__class__.TYPE == "JSON_FRAGMENT":
287
+ run_generator_fn = _run_json_fragment_generator
288
+ add_result_fn = ret.add_json_fragment
289
+ else:
290
+ raise RuntimeError(f"Unknown generator class type: cls={gen.__class__} TYPE={gen.__class__.TYPE}")
291
+ try:
292
+ result = run_generator_fn(gen, device)
293
+ except NotSupportedDevice as exc:
294
+ logger.info("generator %s raised unsupported error: %r", gen, exc)
295
+ continue
296
+ if result:
297
+ add_result_fn(result)
298
+
299
+ return ret
300
+
301
+
302
+ @tracing.function(min_duration="0.5")
303
+ def _run_entire_generator(gen: "Entire", device: "Device") -> Optional[GeneratorResult]:
304
+ logger = get_logger(generator=_make_generator_ctx(gen))
305
+ if not gen.supports_device(device):
306
+ logger.info("generator %s is not supported for device %s", gen, device.hostname)
307
+ return
308
+
309
+ span = tracing_connector.get().get_current_span()
310
+ if span:
311
+ tracing_connector.get().set_device_attributes(span, device)
312
+ tracing_connector.get().set_dimensions_attributes(span, gen, device)
313
+
314
+ path = gen.path(device)
315
+ if not path:
316
+ raise RuntimeError("entire generator should return non-empty path")
317
+
318
+ logger.info("Generating ENTIRE ...")
319
+ with GeneratorPerfMesurer(gen, trace_min_duration="0.5") as pm:
320
+ output = gen(device)
321
+
322
+ return GeneratorEntireResult(
323
+ name=gen.__class__.__name__,
324
+ tags=gen.TAGS,
325
+ path=path,
326
+ output=output,
327
+ reload=gen.get_reload_cmds(device),
328
+ prio=gen.prio,
329
+ perf=pm.last_result,
330
+ is_safe=gen.is_safe(device),
331
+ )
332
+
333
+
334
+ def _make_generator_ctx(gen):
335
+ return "%s.[%s]" % (gen.__module__, gen.__class__.__name__)
336
+
337
+
338
+ def _run_json_fragment_generator(
339
+ gen: "JSONFragment",
340
+ device: "Device",
341
+ ) -> Optional[GeneratorResult]:
342
+ logger = get_logger(generator=_make_generator_ctx(gen))
343
+ if not gen.supports_device(device):
344
+ logger.info("generator %s is not supported for device %s", gen, device.hostname)
345
+ return
346
+
347
+ path = gen.path(device)
348
+ if not path:
349
+ raise RuntimeError("json fragment generator should return non-empty path")
350
+
351
+ acl_item_or_list_of_items = gen.acl(device)
352
+ safe_acl_item_or_list_of_items = gen.acl_safe(device)
353
+ if not acl_item_or_list_of_items:
354
+ raise RuntimeError("json fragment generator should return non-empty acl")
355
+ if isinstance(acl_item_or_list_of_items, list):
356
+ acl = acl_item_or_list_of_items
357
+ else:
358
+ acl = [acl_item_or_list_of_items]
359
+ if isinstance(safe_acl_item_or_list_of_items, list):
360
+ acl_safe = safe_acl_item_or_list_of_items
361
+ else:
362
+ acl_safe = [safe_acl_item_or_list_of_items]
363
+
364
+ logger.info("Generating JSON_FRAGMENT ...")
365
+ with GeneratorPerfMesurer(gen) as pm:
366
+ config = gen(device)
367
+ reload_cmds = gen.get_reload_cmds(device)
368
+ return GeneratorJSONFragmentResult(
369
+ name=gen.__class__.__name__,
370
+ tags=gen.TAGS,
371
+ path=path,
372
+ acl=acl,
373
+ acl_safe=acl_safe,
374
+ config=config,
375
+ reload=reload_cmds,
376
+ perf=pm.last_result,
377
+ reload_prio=gen.reload_prio,
378
+ )
379
+
380
+
381
+ def _get_generators(module_paths: Union[List[str], dict], storage, device=None):
382
+ if isinstance(module_paths, dict):
383
+ if device is None:
384
+ module_paths = module_paths.get("default")
385
+ else:
386
+ modules = []
387
+ seen = set()
388
+ for prop, prop_modules in module_paths.get("per_device_property", {}).items():
389
+ if getattr(device, prop, False) is True:
390
+ for module in prop_modules:
391
+ if module not in seen:
392
+ modules.append(module)
393
+ seen.add(module)
394
+ module_paths = modules or module_paths.get("default")
395
+ res_generators = []
396
+ for module_path in module_paths:
397
+ module = importlib.import_module(module_path)
398
+ if hasattr(module, "get_generators"):
399
+ generators: List[BaseGenerator] = module.get_generators(storage)
400
+ res_generators += generators
401
+ return res_generators
402
+
403
+
404
+ def _get_ref_generators(module_paths: List[str], storage, device):
405
+ if isinstance(module_paths, dict):
406
+ module_paths = module_paths.get("default")
407
+ res_generators = []
408
+ for module_path in module_paths:
409
+ module = importlib.import_module(module_path)
410
+ if hasattr(module, "get_ref_generators"):
411
+ res_generators += module.get_ref_generators(storage)
412
+ return res_generators
413
+
414
+
415
+ def select_generators(gens: GenSelectOptions, classes: Iterable[BaseGenerator]):
416
+ def contains(obj, where):
417
+ if where:
418
+ return obj.get_aliases().intersection(where)
419
+ return False
420
+
421
+ def has(cls, what):
422
+ return what in cls.TAGS
423
+
424
+ flts = [lambda c: not isinstance(c, RefGenerator)]
425
+ if gens.allowed_gens:
426
+ flts.append(lambda c: contains(c, gens.allowed_gens))
427
+ elif gens.force_enabled:
428
+ flts.append(lambda c: not has(c, DISABLED_TAG) or contains(c, gens.force_enabled))
429
+ elif not gens.ignore_disabled:
430
+ flts.append(lambda c: not has(c, DISABLED_TAG))
431
+
432
+ if gens.excluded_gens:
433
+ flts.append(lambda c: not contains(c, gens.excluded_gens))
434
+
435
+ return filter(lambda x: all(f(x) for f in flts), classes)
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import contextlib
5
+ import textwrap
6
+ from typing import Union, List
7
+
8
+ from annet import tabparser, tracing
9
+ from annet.tracing import tracing_connector
10
+ from .exceptions import InvalidValueFromGenerator
11
+
12
+
13
+ class DefaultBlockIfCondition:
14
+ pass
15
+
16
+
17
+ ParamsList = tabparser.JuniperList
18
+
19
+
20
+ class GenStringable(abc.ABC):
21
+ @abc.abstractmethod
22
+ def gen_str(self) -> str:
23
+ pass
24
+
25
+
26
+ def _filter_str(value: Union[
27
+ str, int, float, tabparser.JuniperList, ParamsList, GenStringable]):
28
+ if isinstance(value, (
29
+ str,
30
+ int,
31
+ float,
32
+ tabparser.JuniperList,
33
+ ParamsList,
34
+ )):
35
+ return str(value)
36
+
37
+ if hasattr(value, "gen_str") and callable(value.gen_str):
38
+ return value.gen_str()
39
+
40
+ raise InvalidValueFromGenerator(
41
+ "Invalid yield type: %s(%s)" % (type(value).__name__, value))
42
+
43
+
44
+ def _split_and_strip(text):
45
+ if "\n" in text:
46
+ rows = textwrap.dedent(text).strip().split("\n")
47
+ else:
48
+ rows = [text]
49
+ return rows
50
+
51
+
52
+ # =====
53
+ class BaseGenerator:
54
+ TYPE: str
55
+ TAGS: List[str]
56
+
57
+ def supports_device(self, device) -> bool: # pylint: disable=unused-argument
58
+ return True
59
+
60
+
61
+ class TreeGenerator(BaseGenerator):
62
+ def __init__(self, indent=" "):
63
+ self._indents = []
64
+ self._rows = []
65
+ self._block_path = []
66
+ self._indent = indent
67
+
68
+ @tracing.contextmanager(min_duration="0.1")
69
+ @contextlib.contextmanager
70
+ def block(self, *tokens, indent=None):
71
+ span = tracing_connector.get().get_current_span()
72
+ if span:
73
+ span.set_attribute("tokens", " ".join(map(str, tokens)))
74
+
75
+ indent = self._indent if indent is None else indent
76
+ block = " ".join(map(_filter_str, tokens))
77
+ self._block_path.append(block)
78
+ self._append_text(block)
79
+ self._indents.append(indent)
80
+ yield
81
+ self._indents.pop(-1)
82
+ self._block_path.pop(-1)
83
+
84
+ @contextlib.contextmanager
85
+ def block_if(self, *tokens, condition=DefaultBlockIfCondition):
86
+ if condition is DefaultBlockIfCondition:
87
+ condition = (None not in tokens and "" not in tokens)
88
+ if condition:
89
+ with self.block(*tokens):
90
+ yield
91
+ return
92
+ yield
93
+
94
+ @contextlib.contextmanager
95
+ def multiblock(self, *blocks):
96
+ if blocks:
97
+ blk = blocks[0]
98
+ tokens = blk if isinstance(blk, (list, tuple)) else [blk]
99
+ with self.block(*tokens):
100
+ with self.multiblock(*blocks[1:]):
101
+ yield
102
+ return
103
+ yield
104
+
105
+ @contextlib.contextmanager
106
+ def multiblock_if(self, *blocks, condition=DefaultBlockIfCondition):
107
+ if condition is DefaultBlockIfCondition:
108
+ condition = (None not in blocks)
109
+ if condition:
110
+ if blocks:
111
+ blk = blocks[0]
112
+ tokens = blk if isinstance(blk, (list, tuple)) else [blk]
113
+ with self.block(*tokens):
114
+ with self.multiblock(*blocks[1:]):
115
+ yield
116
+ return
117
+ yield
118
+
119
+ # ===
120
+ def _append_text(self, text):
121
+ self._append_text_cb(text)
122
+
123
+ def _append_text_cb(self, text, row_cb=None):
124
+ for row in _split_and_strip(text):
125
+ if row_cb:
126
+ row = row_cb(row)
127
+ self._rows.append("".join(self._indents) + row)
128
+
129
+
130
+ class TextGenerator(TreeGenerator):
131
+ def __add__(self, line):
132
+ self._append_text(line)
133
+ return self
134
+
135
+ def __iter__(self):
136
+ yield from self._rows
File without changes
@@ -0,0 +1,33 @@
1
+ from annet.generators import PartialGenerator
2
+
3
+
4
+ class InitialConfig(PartialGenerator):
5
+
6
+ """
7
+ Конфиги у свежих (еще ни разу не настраиваемых устройств)
8
+ на самом деле НЕ пустые. В данном генераторе отображен
9
+ такой набор команд, по крайней мере тех, которые могут
10
+ изменяться в ходе первичной конфигурации.
11
+
12
+ Acl для данного генератора не нужен, он будет генерировать
13
+ конфиг целиком.
14
+ """
15
+ def __init__(self, storage=None):
16
+ self._do_run = not storage
17
+ super().__init__(storage=storage)
18
+
19
+ def run_huawei(self, device):
20
+ if not self._do_run:
21
+ return
22
+ if device.hw.CE:
23
+ yield """
24
+ telnet server disable
25
+ telnet ipv6 server disable
26
+ diffserv domain default
27
+ aaa
28
+ authentication-scheme default
29
+ authorization-scheme default
30
+ accounting-scheme default
31
+ domain default
32
+ domain default_admin
33
+ """
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import pkgutil
4
+ import re
5
+ import types
6
+ from typing import (
7
+ FrozenSet,
8
+ Iterable,
9
+ List,
10
+ Optional,
11
+ Set,
12
+ Union,
13
+ )
14
+
15
+ from annet.lib import (
16
+ flatten,
17
+ jinja_render,
18
+ mako_render,
19
+ )
20
+ from .base import BaseGenerator, _filter_str
21
+ from .exceptions import NotSupportedDevice
22
+
23
+
24
+ class Entire(BaseGenerator):
25
+ TYPE = "ENTIRE"
26
+ TAGS: List[str] = []
27
+ REQUIRED_PACKAGES: FrozenSet[str] = frozenset()
28
+
29
+ def __init__(self, storage):
30
+ self.storage = storage
31
+ # между генераторами для одного и того же path - выбирается тот что больше
32
+ if not hasattr(self, "prio"):
33
+ self.prio = 100
34
+ self.__device = None
35
+
36
+ def supports_device(self, device):
37
+ return bool(self.path(device))
38
+
39
+ def run(self, device) -> Union[None, str, Iterable[Union[str, tuple]]]:
40
+ raise NotImplementedError
41
+
42
+ def reload(self, device) -> Optional[
43
+ str]: # pylint: disable=unused-argument
44
+ return
45
+
46
+ def get_reload_cmds(self, device) -> str:
47
+ ret = self.reload(device) or ""
48
+ path = self.path(device)
49
+ if path and device.hw.PC and device.hw.soft.startswith(
50
+ ("Cumulus", "SwitchDev", "SONiC"),
51
+ ):
52
+ parts = []
53
+ if ret:
54
+ parts.append(ret)
55
+ parts.append("/usr/bin/etckeeper commitreload %s" % path)
56
+ return "\n".join(parts)
57
+ return ret
58
+
59
+ def path(self, device) -> Optional[str]:
60
+ raise NotImplementedError("Required PATH for ENTIRE generator")
61
+
62
+ # pylint: disable=unused-argument
63
+ def is_safe(self, device) -> bool:
64
+ """Output gen results when --acl-safe flag is used"""
65
+ return False
66
+
67
+ def read(self, path) -> str:
68
+ return pkgutil.get_data(__name__, path).decode()
69
+
70
+ def mako(self, text, **kwargs) -> str:
71
+ return mako_render(text, dedent=True, device=self.__device, **kwargs)
72
+
73
+ def jinja(self, text, **kwargs) -> str:
74
+ return jinja_render(text, dedent=True, device=self.__device, **kwargs)
75
+
76
+ # =====
77
+
78
+ @classmethod
79
+ def get_aliases(cls) -> Set[str]:
80
+ return {cls.__name__, *cls.TAGS}
81
+
82
+ def __call__(self, device):
83
+ self.__device = device
84
+ parts = []
85
+ run_res = self.run(device)
86
+ if isinstance(run_res, str):
87
+ run_res = (run_res,)
88
+ if run_res is None or not isinstance(run_res, (tuple, types.GeneratorType)):
89
+ raise Exception("generator %s returns %s" % (
90
+ self.__class__.__name__, type(run_res)))
91
+ for text in run_res:
92
+ if isinstance(text, tuple):
93
+ text = " ".join(map(_filter_str, flatten(text)))
94
+ assert re.search(r"\bNone\b", text) is None, \
95
+ "Found 'None' in yield result: %s" % text
96
+ parts.append(text)
97
+ return "\n".join(parts)