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
annet/lib.py ADDED
@@ -0,0 +1,128 @@
1
+ import asyncio
2
+ import os
3
+ import shutil
4
+ import sys
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+ from typing import Awaitable, Optional
8
+
9
+ import yaml
10
+ from annet.annlib.lib import ( # pylint: disable=unused-import
11
+ ContextOrderedDict,
12
+ HuaweiNumBlock,
13
+ LMSMatcher,
14
+ add_annotation,
15
+ catch_ctrl_c,
16
+ cisco_collapse_vlandb,
17
+ cisco_expand_vlandb,
18
+ find_exc_in_stack,
19
+ find_modules,
20
+ first,
21
+ flatten,
22
+ huawei_collapse_vlandb,
23
+ huawei_expand_vlandb,
24
+ huawei_iface_ranges,
25
+ is_relative,
26
+ jinja_render,
27
+ jun_activate,
28
+ jun_is_inactive,
29
+ juniper_fmt_prefix_lists_acl,
30
+ juniper_port_split,
31
+ make_ip4_mask,
32
+ mako_render,
33
+ merge_dicts,
34
+ percentile,
35
+ uniq,
36
+ )
37
+ from contextlog import get_logger
38
+
39
+
40
+ _TEMPLATE_CONTEXT_PATH: Optional[str] = None
41
+ _DEFAULT_CONTEXT_PATH: Optional[str] = None
42
+
43
+
44
+ def get_template_context_path() -> str:
45
+ if _TEMPLATE_CONTEXT_PATH is None:
46
+ set_template_context_path(str(Path(sys.modules["annet"].__file__).parent / "configs/context.yml"))
47
+ return _TEMPLATE_CONTEXT_PATH
48
+
49
+
50
+ def set_template_context_path(path: str) -> None:
51
+ global _TEMPLATE_CONTEXT_PATH # pylint: disable=global-statement
52
+ _TEMPLATE_CONTEXT_PATH = path
53
+
54
+
55
+ def get_default_context_path() -> str:
56
+ if _DEFAULT_CONTEXT_PATH is None:
57
+ set_default_context_path("~/.annet/context.yml")
58
+ return _DEFAULT_CONTEXT_PATH
59
+
60
+
61
+ def set_default_context_path(path: str) -> None:
62
+ global _DEFAULT_CONTEXT_PATH # pylint: disable=global-statement
63
+ _DEFAULT_CONTEXT_PATH = path
64
+
65
+
66
+ @lru_cache(maxsize=1)
67
+ def _get_template_context():
68
+ with open(get_template_context_path()) as f:
69
+ return yaml.safe_load(f)
70
+
71
+
72
+ def get_context_path(touch: Optional[bool] = False) -> str:
73
+ path = Path(os.getenv("ANN_CONTEXT_CONFIG_PATH", get_default_context_path())).expanduser().absolute()
74
+ if not path.exists():
75
+ src = get_template_context_path()
76
+ if not touch:
77
+ return str(src)
78
+ try:
79
+ # populate path with default configuration
80
+ path.parent.mkdir(parents=True, exist_ok=True)
81
+ shutil.copy(src, path)
82
+ except shutil.SameFileError:
83
+ pass
84
+ return str(path)
85
+
86
+
87
+ def get_context() -> dict:
88
+ with open(get_context_path()) as f:
89
+ raw = yaml.safe_load(f)
90
+ _fill_in_default_generator_modules(raw)
91
+ context_name = os.getenv("ANN_SELECTED_CONTEXT", raw["selected_context"])
92
+ res = {k: raw[k][v] for k, v in raw["context"][context_name].items()}
93
+ if "ANN_GENERATORS_CONTEXT" in os.environ: # an undocumented hack to maintain backwards compatibility; TODO: remove
94
+ res["generators"] = raw["generators"][os.getenv("ANN_GENERATORS_CONTEXT")]
95
+ return res
96
+
97
+
98
+ @lru_cache(maxsize=1)
99
+ def _warn_no_generators_in_context():
100
+ get_logger().warning(
101
+ "Older version of the context configuration found. Getting generators references from the template context"
102
+ )
103
+
104
+
105
+ def _fill_in_default_generator_modules(raw: dict) -> bool:
106
+ """Backwards compatibility hack to add existing generators refs to context"""
107
+ if "generators" not in raw:
108
+ _warn_no_generators_in_context()
109
+ raw["generators"] = _get_template_context()["generators"]
110
+ for dst_context in raw["context"].values():
111
+ dst_context["generators"] = "default"
112
+ return True
113
+ return False
114
+
115
+
116
+ def repair_context_file() -> None:
117
+ path = get_context_path()
118
+ with open(path) as f:
119
+ data = yaml.safe_load(f)
120
+ if _fill_in_default_generator_modules(data):
121
+ with open(path, "w") as f:
122
+ yaml.dump(data, f, sort_keys=False)
123
+
124
+
125
+ def do_async(coro: Awaitable):
126
+ loop = asyncio.get_event_loop()
127
+ res = loop.run_until_complete(coro)
128
+ return res
annet/output.py ADDED
@@ -0,0 +1,167 @@
1
+ import abc
2
+ import os
3
+ import posixpath
4
+ import sys
5
+ from typing import List, Optional, Tuple, Type, Dict
6
+
7
+ import colorama
8
+ from annet.annlib.output import ( # pylint: disable=unused-import
9
+ LABEL_NEW_PREFIX,
10
+ OutputWriter,
11
+ TextArgs,
12
+ capture_output,
13
+ dir_or_file_output,
14
+ format_file_diff,
15
+ print_as_json,
16
+ print_as_yaml,
17
+ print_err_label,
18
+ print_label,
19
+ )
20
+ from contextlog import get_logger
21
+
22
+ from annet.cli_args import FileOutOptions, QueryOptions
23
+ from annet.connectors import Connector
24
+ from annet.storage import Device, storage_connector
25
+
26
+
27
+ class _DriverConnector(Connector["OutputDriver"]):
28
+ name = "OutputDriver"
29
+ ep_name = "output"
30
+
31
+ def _get_default(self) -> Type["OutputDriver"]:
32
+ return OutputDriverBasic
33
+
34
+
35
+ output_driver_connector = _DriverConnector()
36
+
37
+
38
+ class OutputDriver(abc.ABC):
39
+ @abc.abstractmethod
40
+ def write_output(self, arg_out: FileOutOptions, items, query_result_count=1) -> None:
41
+ pass
42
+
43
+ @abc.abstractmethod
44
+ def format_fails(self, fail, fqdns: Optional[Dict[int, str]] = None) -> Tuple[str, str]:
45
+ pass
46
+
47
+ @abc.abstractmethod
48
+ def cfg_file_names(self, device: Device) -> List[str]:
49
+ pass
50
+
51
+ @abc.abstractmethod
52
+ def entire_config_dest_path(self, device: Device, config_path: str) -> str:
53
+ pass
54
+
55
+
56
+ class OutputDriverBasic(OutputDriver):
57
+ def write_output(self, arg_out: FileOutOptions, items, query_result_count=1):
58
+ """
59
+ пишет результаты генерации в файл или директорию :dest
60
+ :dest - это директория в случаях:
61
+ - заканчивается на "/"
62
+ - существует директория с таким именем
63
+ - более одного устройства в результате запроса
64
+ - есть entire-генераторы (определяется по типу первого результата, если устройство одно)
65
+ """
66
+ logger = get_logger()
67
+
68
+ items_iter = iter(items)
69
+ try:
70
+ first_result = next(items_iter)
71
+ except StopIteration:
72
+ # нет результатов, ничего не пишем и не создаём
73
+ return
74
+
75
+ def _reassemble_items():
76
+ yield first_result
77
+ yield from items_iter
78
+
79
+ dest = arg_out.dest
80
+ dir_mode = dir_or_file_output(dest, query_result_count, suggest_dir=(os.sep in first_result[0]))
81
+ _reassembled_items = list(_reassemble_items())
82
+
83
+ for output_no, (label, output, is_fail) in enumerate(_reassembled_items):
84
+ writer = output if isinstance(output, OutputWriter) else OutputWriter(output)
85
+ label = os.path.normpath(label)
86
+ label_color = colorama.Back.RED if is_fail else colorama.Back.GREEN
87
+ if dest is None:
88
+ if hasattr(arg_out, "format") and arg_out.format == "json":
89
+ if output_no > 0:
90
+ sys.stdout.write(",")
91
+ elif output_no == 0:
92
+ sys.stdout.write("{")
93
+ sys.stdout.write('"%s": ' % label)
94
+ writer.write(sys.stdout)
95
+ if output_no == len(_reassembled_items) - 1:
96
+ sys.stdout.write("}")
97
+ else:
98
+ if not arg_out.no_label:
99
+ print_label(label, back_color=label_color)
100
+ writer.write(sys.stdout)
101
+ elif dir_mode:
102
+ if label.startswith(LABEL_NEW_PREFIX):
103
+ label = label[len(LABEL_NEW_PREFIX):]
104
+ if label.startswith(os.sep):
105
+ # just in case.
106
+ label = label.lstrip(os.sep)
107
+ if os.sep not in label:
108
+ # vendor config
109
+ label = os.path.basename(label)
110
+ else:
111
+ # entire generated file
112
+ parts = label.split(os.sep)
113
+ hostname = parts[0]
114
+ label = os.sep.join(parts[1:])
115
+ if not arg_out.expand_path:
116
+ label = os.path.basename(label)
117
+ if query_result_count > 1:
118
+ label = os.path.join(hostname, label)
119
+ file_dest = os.path.join(dest, "errors") if is_fail else dest
120
+ out_file = os.path.normpath(os.path.join(file_dest, label))
121
+ logger.info("writing '%s'", out_file)
122
+ dirname = os.path.dirname(out_file)
123
+ if not os.path.exists(dirname):
124
+ os.makedirs(dirname)
125
+ with open(out_file, "w") as file:
126
+ writer.write(file)
127
+ else:
128
+ logger.info("writing '%s'", dest)
129
+ with open(dest, "w") as file:
130
+ writer.write(file)
131
+
132
+ def format_fails(self, fail, fqdns: Optional[Dict[int, str]] = None):
133
+ ret = []
134
+ fqdns = fqdns or {}
135
+ for (assignment, exc) in fail.items():
136
+ label = assignment
137
+ if assignment in fqdns:
138
+ label = fqdns[assignment]
139
+ elif isinstance(assignment, tuple):
140
+ label = assignment[0]
141
+ else:
142
+ ValueError("Failed to parse failed assignment %r" % assignment)
143
+ ret.append((label, getattr(exc, "formatted_output", f"{repr(exc)} (formatted_output is absent)"), True))
144
+ return ret
145
+
146
+ def cfg_file_names(self, device: Device) -> List[str]:
147
+ return [f"{device.hostname}.cfg"]
148
+
149
+ def entire_config_dest_path(self, device, config_path: str) -> str:
150
+ """Формирует путь к конфигу в директории destname.
151
+
152
+ Например, для устройства с hostname `my-device`:
153
+ ```
154
+ >>> device.entire_config_dest_path("/etc/frr/frr.conf")
155
+ 'my-device.cfg/etc/frr/frr.conf'
156
+ >>>
157
+ ```
158
+ """
159
+ # NOTE: с полученным `config_path` работаем через `posixpath`, а не через `os.path`, потому что
160
+ # entire-путь POSIX-специфичный; но в конце формируем путь через `os.path` для текущей платформы
161
+ if not posixpath.abspath(config_path):
162
+ raise RuntimeError(f"Want absolute config path, but relative received: {config_path}")
163
+ cfg_files = self.cfg_file_names(device)
164
+ # NOTE: получаем путь без "/" в начале, например, "etc/frr/frr.conf"
165
+ relative_config_path = posixpath.relpath(config_path, "/")
166
+ dest_config_path_parts = [cfg_files[0]] + relative_config_path.split(posixpath.sep)
167
+ return os.path.join(*dest_config_path_parts)