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.
- annet/__init__.py +61 -0
- 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 +105 -0
- annet/adapters/netbox/common/query.py +23 -0
- annet/adapters/netbox/common/status_client.py +25 -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 +73 -0
- annet/adapters/netbox/v24/client.py +59 -0
- annet/adapters/netbox/v24/storage.py +196 -0
- annet/adapters/netbox/v37/__init__.py +0 -0
- annet/adapters/netbox/v37/api_models.py +38 -0
- annet/adapters/netbox/v37/client.py +62 -0
- annet/adapters/netbox/v37/storage.py +149 -0
- annet/annet.py +25 -0
- annet/annlib/__init__.py +7 -0
- annet/annlib/command.py +49 -0
- annet/annlib/diff.py +158 -0
- annet/annlib/errors.py +8 -0
- annet/annlib/filter_acl.py +196 -0
- annet/annlib/jsontools.py +116 -0
- annet/annlib/lib.py +495 -0
- annet/annlib/netdev/__init__.py +0 -0
- annet/annlib/netdev/db.py +62 -0
- annet/annlib/netdev/devdb/__init__.py +28 -0
- annet/annlib/netdev/devdb/data/devdb.json +137 -0
- annet/annlib/netdev/views/__init__.py +0 -0
- annet/annlib/netdev/views/dump.py +121 -0
- annet/annlib/netdev/views/hardware.py +112 -0
- annet/annlib/output.py +246 -0
- annet/annlib/patching.py +533 -0
- annet/annlib/rbparser/__init__.py +0 -0
- annet/annlib/rbparser/acl.py +120 -0
- annet/annlib/rbparser/deploying.py +55 -0
- annet/annlib/rbparser/ordering.py +52 -0
- annet/annlib/rbparser/platform.py +51 -0
- annet/annlib/rbparser/syntax.py +115 -0
- annet/annlib/rulebook/__init__.py +0 -0
- annet/annlib/rulebook/common.py +350 -0
- annet/annlib/tabparser.py +648 -0
- annet/annlib/types.py +35 -0
- annet/api/__init__.py +826 -0
- annet/argparse.py +415 -0
- annet/cli.py +237 -0
- annet/cli_args.py +503 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +77 -0
- annet/deploy.py +536 -0
- annet/diff.py +84 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +865 -0
- annet/generators/__init__.py +435 -0
- annet/generators/base.py +136 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- 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/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +167 -0
- annet/parallel.py +448 -0
- annet/patching.py +25 -0
- annet/reference.py +148 -0
- annet/rulebook/__init__.py +114 -0
- annet/rulebook/arista/__init__.py +0 -0
- annet/rulebook/arista/iface.py +16 -0
- annet/rulebook/aruba/__init__.py +16 -0
- annet/rulebook/aruba/ap_env.py +146 -0
- annet/rulebook/aruba/misc.py +8 -0
- annet/rulebook/cisco/__init__.py +0 -0
- annet/rulebook/cisco/iface.py +68 -0
- annet/rulebook/cisco/misc.py +57 -0
- annet/rulebook/cisco/vlandb.py +90 -0
- annet/rulebook/common.py +19 -0
- annet/rulebook/deploying.py +87 -0
- annet/rulebook/huawei/__init__.py +0 -0
- annet/rulebook/huawei/aaa.py +75 -0
- annet/rulebook/huawei/bgp.py +97 -0
- annet/rulebook/huawei/iface.py +33 -0
- annet/rulebook/huawei/misc.py +337 -0
- annet/rulebook/huawei/vlandb.py +115 -0
- annet/rulebook/juniper/__init__.py +107 -0
- annet/rulebook/nexus/__init__.py +0 -0
- annet/rulebook/nexus/iface.py +92 -0
- annet/rulebook/patching.py +143 -0
- annet/rulebook/ribbon/__init__.py +12 -0
- annet/rulebook/texts/arista.deploy +20 -0
- annet/rulebook/texts/arista.order +125 -0
- annet/rulebook/texts/arista.rul +59 -0
- annet/rulebook/texts/aruba.deploy +20 -0
- annet/rulebook/texts/aruba.order +83 -0
- annet/rulebook/texts/aruba.rul +87 -0
- annet/rulebook/texts/cisco.deploy +27 -0
- annet/rulebook/texts/cisco.order +82 -0
- annet/rulebook/texts/cisco.rul +105 -0
- annet/rulebook/texts/huawei.deploy +188 -0
- annet/rulebook/texts/huawei.order +388 -0
- annet/rulebook/texts/huawei.rul +471 -0
- annet/rulebook/texts/juniper.rul +120 -0
- annet/rulebook/texts/nexus.deploy +24 -0
- annet/rulebook/texts/nexus.order +85 -0
- annet/rulebook/texts/nexus.rul +83 -0
- annet/rulebook/texts/nokia.rul +31 -0
- annet/rulebook/texts/pc.order +5 -0
- annet/rulebook/texts/pc.rul +9 -0
- annet/rulebook/texts/ribbon.deploy +22 -0
- annet/rulebook/texts/ribbon.rul +77 -0
- annet/rulebook/texts/routeros.order +38 -0
- annet/rulebook/texts/routeros.rul +45 -0
- annet/storage.py +125 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +227 -0
- annet-0.0.dist-info/AUTHORS +21 -0
- annet-0.0.dist-info/LICENSE +21 -0
- annet-0.0.dist-info/METADATA +26 -0
- annet-0.0.dist-info/RECORD +137 -0
- annet-0.0.dist-info/WHEEL +5 -0
- annet-0.0.dist-info/entry_points.txt +5 -0
- annet-0.0.dist-info/top_level.txt +2 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- 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)
|
annet/generators/base.py
ADDED
|
@@ -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)
|