annet 0.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/__init__.py +61 -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 +89 -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 +807 -0
- annet/argparse.py +415 -0
- annet/cli.py +192 -0
- annet/cli_args.py +493 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +64 -0
- annet/deploy.py +441 -0
- annet/diff.py +85 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +828 -0
- annet/generators/__init__.py +987 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- annet/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +170 -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 +121 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +223 -0
- annet-0.1.dist-info/AUTHORS +21 -0
- annet-0.1.dist-info/LICENSE +21 -0
- annet-0.1.dist-info/METADATA +24 -0
- annet-0.1.dist-info/RECORD +113 -0
- annet-0.1.dist-info/WHEEL +5 -0
- annet-0.1.dist-info/entry_points.txt +6 -0
- annet-0.1.dist-info/top_level.txt +3 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- annet_generators/example/lldp.py +52 -0
- annet_nbexport/__init__.py +220 -0
- annet_nbexport/main.py +46 -0
|
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=None)
|
|
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
|
+
"""
|
annet/hardware.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from annet.annlib.netdev.views.hardware import HardwareView, hw_to_vendor
|
|
5
|
+
|
|
6
|
+
from annet.connectors import Connector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from annet.annlib.netdev.views.hardware import vendor_to_hw
|
|
11
|
+
except ImportError:
|
|
12
|
+
from netdev.views.hardware import vendor_to_hw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _HardwareConnector(Connector["HarwareProvider"]):
|
|
16
|
+
name = "Hardware"
|
|
17
|
+
ep_name = "hardware"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
hardware_connector = _HardwareConnector()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HarwareProvider(abc.ABC):
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
def make_hw(self, hw_model: str, sw_version: str) -> Any:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def vendor_to_hw(self, vendor: str) -> Any:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abc.abstractmethod
|
|
33
|
+
def hw_to_vendor(self, hw: Any) -> str:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AnnetHardwareProvider(HarwareProvider):
|
|
38
|
+
def make_hw(self, hw_model: str, sw_version: str) -> HardwareView:
|
|
39
|
+
return HardwareView(hw_model, sw_version)
|
|
40
|
+
|
|
41
|
+
def vendor_to_hw(self, vendor: str) -> HardwareView:
|
|
42
|
+
return vendor_to_hw(vendor)
|
|
43
|
+
|
|
44
|
+
def hw_to_vendor(self, hw: HardwareView) -> str:
|
|
45
|
+
return hw_to_vendor(hw)
|
annet/implicit.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from collections import OrderedDict as odict
|
|
2
|
+
|
|
3
|
+
from annet.annlib.rbparser import syntax
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def config(config_tree, rules):
|
|
7
|
+
implicit_config_tree = odict()
|
|
8
|
+
for (row, rule) in rules.items():
|
|
9
|
+
matched_lines = [line for line in config_tree.keys() if rule["regexp"].match(line)]
|
|
10
|
+
if rule["type"] != "ignore":
|
|
11
|
+
if not any(matched_lines) and row not in config_tree:
|
|
12
|
+
implicit_config_tree[row] = odict()
|
|
13
|
+
for line in matched_lines:
|
|
14
|
+
implicit_config_tree[line] = config(config_tree[line], rule["children"])
|
|
15
|
+
return implicit_config_tree
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compile_rules(device):
|
|
19
|
+
return compile_tree(_implicit_tree(device))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compile_tree(tree):
|
|
23
|
+
rules = odict()
|
|
24
|
+
for (_, attrs) in tree.items():
|
|
25
|
+
rule = {
|
|
26
|
+
"type": attrs["type"],
|
|
27
|
+
"children": compile_tree(attrs["children"]) if attrs.get("children") else odict(),
|
|
28
|
+
"regexp": syntax.compile_row_regexp(attrs["row"]),
|
|
29
|
+
}
|
|
30
|
+
rules[attrs["row"]] = rule
|
|
31
|
+
return rules
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _implicit_tree(device):
|
|
35
|
+
text = ""
|
|
36
|
+
if device.hw.Huawei:
|
|
37
|
+
if device.hw.Huawei.CE:
|
|
38
|
+
text = """
|
|
39
|
+
stp mode mstp
|
|
40
|
+
stp enable
|
|
41
|
+
undo ntp server disable
|
|
42
|
+
undo telnet server disable
|
|
43
|
+
undo dhcp enable
|
|
44
|
+
!user-interface con *
|
|
45
|
+
user privilege level 3
|
|
46
|
+
!user-interface vty ~
|
|
47
|
+
protocol inbound all
|
|
48
|
+
netconf
|
|
49
|
+
"""
|
|
50
|
+
elif device.hw.Huawei.NE:
|
|
51
|
+
text = """
|
|
52
|
+
!bgp *
|
|
53
|
+
!ipv4-family unicast
|
|
54
|
+
undo synchronization
|
|
55
|
+
!ipv6-family unicast
|
|
56
|
+
undo synchronization
|
|
57
|
+
!user-interface con *
|
|
58
|
+
user privilege level 3
|
|
59
|
+
!user-interface vty ~
|
|
60
|
+
protocol inbound all
|
|
61
|
+
aaa
|
|
62
|
+
undo user-password complexity-check
|
|
63
|
+
netconf
|
|
64
|
+
"""
|
|
65
|
+
else:
|
|
66
|
+
text = """
|
|
67
|
+
stp mode mstp
|
|
68
|
+
!interface X?GigabitEthernet*
|
|
69
|
+
bpdu enable
|
|
70
|
+
netconf
|
|
71
|
+
"""
|
|
72
|
+
elif device.hw.Arista:
|
|
73
|
+
# эта часть конфигурации будет не видна в конфиге, если она включена с таким набором полей:
|
|
74
|
+
text = r"""
|
|
75
|
+
ip load-sharing trident fields ipv6 destination-port source-ip ingress-interface destination-ip source-port flow-label
|
|
76
|
+
ip load-sharing trident fields ip source-ip source-port destination-ip destination-port ingress-interface
|
|
77
|
+
"""
|
|
78
|
+
elif device.hw.Nexus:
|
|
79
|
+
text = r"""
|
|
80
|
+
# часть конфигурации скрытая если включена
|
|
81
|
+
snmp-server enable traps link linkDown
|
|
82
|
+
snmp-server enable traps link linkUp
|
|
83
|
+
"""
|
|
84
|
+
if device.hw.Nexus.N3x.N3432 \
|
|
85
|
+
or (device.hw.Nexus.N9x.N9500 and "spine1" in device.tags) \
|
|
86
|
+
or device.hw.Nexus.N9x.N9316 or device.hw.Cisco.Nexus.N9x.N9364:
|
|
87
|
+
text += r"""
|
|
88
|
+
# SVI
|
|
89
|
+
!interface Vlan*
|
|
90
|
+
!shutdown
|
|
91
|
+
!interface mgmt[0-9]*
|
|
92
|
+
no shutdown
|
|
93
|
+
mtu 1500
|
|
94
|
+
!interface Ethernet1*
|
|
95
|
+
no shutdown
|
|
96
|
+
# Лупбеки
|
|
97
|
+
!interface */Loopback[0-9.]+/
|
|
98
|
+
no shutdown
|
|
99
|
+
# Агрегаты
|
|
100
|
+
!interface */port-channel[0-9.]+/
|
|
101
|
+
no shutdown
|
|
102
|
+
# BGP
|
|
103
|
+
!router bgp *
|
|
104
|
+
!neighbor *
|
|
105
|
+
no shutdown
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
elif device.hw.Nexus.N3x:
|
|
109
|
+
# У нексуса сложные взаимоотношения с
|
|
110
|
+
# shutdown командой вездe
|
|
111
|
+
# в данный момент поведение проверенно для Cisco Nexus 3132Q 6.0(2)U6(7)
|
|
112
|
+
text += r"""
|
|
113
|
+
# SVI
|
|
114
|
+
!interface Vlan*
|
|
115
|
+
!shutdown
|
|
116
|
+
!interface mgmt[0-9]*
|
|
117
|
+
no shutdown
|
|
118
|
+
# Физические НЕ сплитованные интерфейсы и subif'ы
|
|
119
|
+
!interface */Ethernet1\/[0-9.]*/
|
|
120
|
+
no shutdown
|
|
121
|
+
# Физические НЕ сплитованные интерфейсы и subif'ы
|
|
122
|
+
!interface */Ethernet1\/[0-9]+\/[0-9.]+/
|
|
123
|
+
# только explicit
|
|
124
|
+
# Лупбеки
|
|
125
|
+
!interface */Loopback[0-9.]+/
|
|
126
|
+
no shutdown
|
|
127
|
+
# Агрегаты
|
|
128
|
+
!interface */port-channel[0-9.]+/
|
|
129
|
+
no shutdown
|
|
130
|
+
# BGP
|
|
131
|
+
!router bgp *
|
|
132
|
+
!neighbor *
|
|
133
|
+
no shutdown
|
|
134
|
+
"""
|
|
135
|
+
return parse_text(text)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def parse_text(text):
|
|
139
|
+
return syntax.parse_text(text, {})
|
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,170 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import os
|
|
3
|
+
import posixpath
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List, Optional, Tuple, Type
|
|
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, args: Optional[QueryOptions] = 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, args: Optional[QueryOptions] = None):
|
|
133
|
+
ret = []
|
|
134
|
+
fqdns = {}
|
|
135
|
+
if args:
|
|
136
|
+
with storage_connector.get().storage()(args) as storage:
|
|
137
|
+
fqdns = storage.resolve_fdnds_by_query(args.query)
|
|
138
|
+
for (assignment, exc) in fail.items():
|
|
139
|
+
label = assignment
|
|
140
|
+
if assignment in fqdns:
|
|
141
|
+
label = fqdns[assignment]
|
|
142
|
+
elif isinstance(assignment, tuple):
|
|
143
|
+
label = assignment[0]
|
|
144
|
+
else:
|
|
145
|
+
ValueError("Failed to parse failed assignment %r" % assignment)
|
|
146
|
+
ret.append((label, getattr(exc, "formatted_output", f"{repr(exc)} (formatted_output is absent)"), True))
|
|
147
|
+
return ret
|
|
148
|
+
|
|
149
|
+
def cfg_file_names(self, device: Device) -> List[str]:
|
|
150
|
+
return [f"{device.hostname}.cfg"]
|
|
151
|
+
|
|
152
|
+
def entire_config_dest_path(self, device, config_path: str) -> str:
|
|
153
|
+
"""Формирует путь к конфигу в директории destname.
|
|
154
|
+
|
|
155
|
+
Например, для устройства с hostname `my-device`:
|
|
156
|
+
```
|
|
157
|
+
>>> device.entire_config_dest_path("/etc/frr/frr.conf")
|
|
158
|
+
'my-device.cfg/etc/frr/frr.conf'
|
|
159
|
+
>>>
|
|
160
|
+
```
|
|
161
|
+
"""
|
|
162
|
+
# NOTE: с полученным `config_path` работаем через `posixpath`, а не через `os.path`, потому что
|
|
163
|
+
# entire-путь POSIX-специфичный; но в конце формируем путь через `os.path` для текущей платформы
|
|
164
|
+
if not posixpath.abspath(config_path):
|
|
165
|
+
raise RuntimeError(f"Want absolute config path, but relative received: {config_path}")
|
|
166
|
+
cfg_files = self.cfg_file_names(device)
|
|
167
|
+
# NOTE: получаем путь без "/" в начале, например, "etc/frr/frr.conf"
|
|
168
|
+
relative_config_path = posixpath.relpath(config_path, "/")
|
|
169
|
+
dest_config_path_parts = [cfg_files[0]] + relative_config_path.split(posixpath.sep)
|
|
170
|
+
return os.path.join(*dest_config_path_parts)
|