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,114 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from os import path
|
|
4
|
+
from typing import Iterable, Union
|
|
5
|
+
|
|
6
|
+
from annet.annlib.lib import mako_render
|
|
7
|
+
from annet.annlib.rbparser.ordering import compile_ordering_text
|
|
8
|
+
from annet.annlib.rbparser.platform import VENDOR_REVERSES
|
|
9
|
+
|
|
10
|
+
from annet.connectors import CachedConnector
|
|
11
|
+
from annet.rulebook.deploying import compile_deploying_text
|
|
12
|
+
from annet.rulebook.patching import compile_patching_text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RulebookProvider(ABC):
|
|
16
|
+
def get_rulebook(self, hw):
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def get_root_modules(self) -> Iterable[str]:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _RulebookProviderConnector(CachedConnector[RulebookProvider]):
|
|
24
|
+
name = "Rulebook provider"
|
|
25
|
+
ep_name = "rulebook"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
rulebook_provider_connector = _RulebookProviderConnector()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_rulebook(hw):
|
|
32
|
+
return rulebook_provider_connector.get().get_rulebook(hw)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DefaultRulebookProvider(RulebookProvider):
|
|
36
|
+
root_dir = (path.dirname(__file__),)
|
|
37
|
+
root_modules = ("annet.rulebook",)
|
|
38
|
+
|
|
39
|
+
def __init__(self, root_dir: Union[str, Iterable[str], None] = None,
|
|
40
|
+
root_modules: Union[str, Iterable[str], None] = None):
|
|
41
|
+
self._rulebook_cache = {}
|
|
42
|
+
self._render_rul_cache = {}
|
|
43
|
+
self._escaped_rul_cache = {}
|
|
44
|
+
|
|
45
|
+
if root_dir is None:
|
|
46
|
+
pass
|
|
47
|
+
elif isinstance(root_dir, str):
|
|
48
|
+
self.root_dir = (root_dir,)
|
|
49
|
+
else:
|
|
50
|
+
self.root_dir = tuple(root_dir)
|
|
51
|
+
|
|
52
|
+
if root_modules is None:
|
|
53
|
+
pass
|
|
54
|
+
elif isinstance(root_modules, str):
|
|
55
|
+
self.root_modules = (root_modules,)
|
|
56
|
+
else:
|
|
57
|
+
self.root_modules = tuple(root_modules)
|
|
58
|
+
|
|
59
|
+
def get_root_modules(self):
|
|
60
|
+
return self.root_modules
|
|
61
|
+
|
|
62
|
+
def get_rulebook(self, hw):
|
|
63
|
+
if hw in self._rulebook_cache:
|
|
64
|
+
return self._rulebook_cache[hw]
|
|
65
|
+
|
|
66
|
+
assert hw.vendor in VENDOR_REVERSES, "Unknown vendor: %s" % (hw.vendor)
|
|
67
|
+
patching = compile_patching_text(self._render_rul(hw.vendor + ".rul", hw), hw.vendor)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
ordering_text = self._render_rul(hw.vendor + ".order", hw)
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
ordering_text = ""
|
|
73
|
+
ordering = compile_ordering_text(ordering_text, hw.vendor)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
deploying_text = self._render_rul(hw.vendor + ".deploy", hw)
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
deploying_text = ""
|
|
79
|
+
|
|
80
|
+
deploying = compile_deploying_text(deploying_text, hw.vendor)
|
|
81
|
+
|
|
82
|
+
self._rulebook_cache[hw] = {
|
|
83
|
+
"patching": patching,
|
|
84
|
+
"ordering": ordering,
|
|
85
|
+
"deploying": deploying,
|
|
86
|
+
}
|
|
87
|
+
return self._rulebook_cache[hw]
|
|
88
|
+
|
|
89
|
+
def _render_rul(self, name, hw):
|
|
90
|
+
key = (name, hw)
|
|
91
|
+
if key not in self._render_rul_cache:
|
|
92
|
+
self._render_rul_cache[key] = mako_render(self._read_escaped_rul(name), hw=hw)
|
|
93
|
+
return self._render_rul_cache[key]
|
|
94
|
+
|
|
95
|
+
def _read_escaped_rul(self, name):
|
|
96
|
+
if name in self._escaped_rul_cache:
|
|
97
|
+
return self._escaped_rul_cache[name]
|
|
98
|
+
for root_dir in self.root_dir:
|
|
99
|
+
try:
|
|
100
|
+
with open(path.join(root_dir, "texts", name), "r") as f:
|
|
101
|
+
self._escaped_rul_cache[name] = self._escape_mako(f.read())
|
|
102
|
+
return self._escaped_rul_cache[name]
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
raise FileNotFoundError(f"Unable to find rul: {name}")
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _escape_mako(text):
|
|
110
|
+
# Экранирование всего, что начинается на %, например %comment -> %%comment, чтобы он не интерпретировался
|
|
111
|
+
# как mako-оператор
|
|
112
|
+
text = re.sub(r"(?:^|\n)%((?!if\s*|elif\s*|else\s*|endif\s*|for\s*|endfor\s*))", "\n%%\\1", text)
|
|
113
|
+
text = re.sub(r"(?:^|\n)\s*#.*", "", text)
|
|
114
|
+
return text
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from annet.rulebook import common
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Добавление возможности удаления агрегатов, лупбэков, SVI и сабинтерфейсов
|
|
7
|
+
def permanent(rule, key, diff, **kwargs): # pylint: disable=redefined-outer-name
|
|
8
|
+
ifname = key[0]
|
|
9
|
+
# Match group examples:
|
|
10
|
+
# Group 01: Port-Channel10, Loopback1, Vlan800
|
|
11
|
+
# Group 02: Ethernet2/1.20, Port-Channel10.200
|
|
12
|
+
if re.match(r"((?:Port-Channel|Loopback|Vlan)\d+$)|((?:Ethernet|Port-Channel)[\d/]+\.\d+$)", ifname):
|
|
13
|
+
# Эти интерфейсы можно удалять
|
|
14
|
+
yield from common.default(rule, key, diff, **kwargs)
|
|
15
|
+
else:
|
|
16
|
+
yield from common.permanent(rule, key, diff, **kwargs)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from annet.annlib.rulebook import common
|
|
2
|
+
from annet.annlib.types import Op
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def default_diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
|
|
6
|
+
diff = common.base_diff(old, new, diff_pre, _pops, moved_to_affected=True)
|
|
7
|
+
diff[:] = _skip_non_ap_env_affected(diff)
|
|
8
|
+
return diff
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _skip_non_ap_env_affected(diff):
|
|
12
|
+
for x in diff:
|
|
13
|
+
if x.op == Op.AFFECTED and not x.children:
|
|
14
|
+
if x.diff_pre["attrs"]["context"].get("block") != "ap-env":
|
|
15
|
+
continue
|
|
16
|
+
yield x
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# pylint: disable=unused-argument
|
|
2
|
+
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from annet.executor import CommandList, Command
|
|
8
|
+
except ImportError:
|
|
9
|
+
from noc.annushka.annet.executor import CommandList, Command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def apply(hw, do_commit, do_finalize, **_):
|
|
13
|
+
before, after = CommandList(), CommandList()
|
|
14
|
+
if do_commit:
|
|
15
|
+
after.add_cmd(Command("write memory"))
|
|
16
|
+
return (before, after)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def patch_flag(rule, key, diff, **_):
|
|
20
|
+
direct, cmd = None, ""
|
|
21
|
+
if diff[Op.ADDED]:
|
|
22
|
+
row, _ = diff[Op.ADDED][0]["row"].split(":")
|
|
23
|
+
cmd = row.replace("_", "-")
|
|
24
|
+
direct = True
|
|
25
|
+
elif diff[Op.REMOVED]:
|
|
26
|
+
row, _ = diff[Op.REMOVED][0]["row"].split(":")
|
|
27
|
+
cmd = "no " + row.replace("_", "-")
|
|
28
|
+
direct = False
|
|
29
|
+
if cmd:
|
|
30
|
+
yield direct, cmd, None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def hostname(rule, key, diff, **_):
|
|
34
|
+
if diff[Op.ADDED]:
|
|
35
|
+
yield True, "hostname %s" % key, None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def mgmt(rule, key, diff, rule_pre, **_):
|
|
39
|
+
if not diff[Op.ADDED] and not diff[Op.REMOVED]:
|
|
40
|
+
return
|
|
41
|
+
pre_items = rule_pre["items"]
|
|
42
|
+
unchanged = {k[0]: v[Op.UNCHANGED][0]["row"].split(":")[1] for k, v in pre_items.items() if v[Op.UNCHANGED]}
|
|
43
|
+
added = {k[0]: v[Op.ADDED][0]["row"].split(":")[1] for k, v in pre_items.items() if v[Op.ADDED]}
|
|
44
|
+
params = {
|
|
45
|
+
"ipaddr": None,
|
|
46
|
+
"netmask": None,
|
|
47
|
+
"gatewayip": None,
|
|
48
|
+
"dnsip": None,
|
|
49
|
+
"domainname": None,
|
|
50
|
+
}
|
|
51
|
+
params.update({k: v for k, v in unchanged.items() if k in params})
|
|
52
|
+
params.update({k: v for k, v in added.items() if k in params})
|
|
53
|
+
empty = {k: v for k, v in params.items() if v is None}
|
|
54
|
+
if empty:
|
|
55
|
+
raise RuntimeError("Failed to determine params %s" % ",".join(empty.keys()))
|
|
56
|
+
yield True, f"ip-address {params['ipaddr']} {params['netmask']} {params['gatewayip']} {params['dnsip']} {params['domainname']}", None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def swarm_mode(rule, key, diff, **_):
|
|
60
|
+
if diff[Op.ADDED]:
|
|
61
|
+
row = diff[Op.ADDED][0]["row"]
|
|
62
|
+
mode = row.split("_")[0]
|
|
63
|
+
yield True, "swarm-mode %s" % mode, None
|
|
64
|
+
elif diff[Op.REMOVED]:
|
|
65
|
+
yield True, "swarm-mode cluster", None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def iap_zone(rule, key, diff, **_):
|
|
69
|
+
if diff[Op.ADDED]:
|
|
70
|
+
yield True, "zone %s" % key, None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def dot11_radio(rule, key, diff, **_):
|
|
74
|
+
direct, cmd = None, ""
|
|
75
|
+
if diff[Op.ADDED]:
|
|
76
|
+
direct, cmd = True, diff[Op.ADDED][0]["row"]
|
|
77
|
+
elif diff[Op.REMOVED]:
|
|
78
|
+
direct, cmd = False, "no " + diff[Op.REMOVED][0]["row"]
|
|
79
|
+
if cmd:
|
|
80
|
+
cmd = cmd.replace("_", "-")
|
|
81
|
+
cmd = cmd.replace(":", "-")
|
|
82
|
+
yield direct, cmd, None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def installation_type(rule, key, diff, **_):
|
|
86
|
+
if diff[Op.ADDED]:
|
|
87
|
+
row = diff[Op.ADDED][0]["row"]
|
|
88
|
+
_, installation_place = row.split(":")
|
|
89
|
+
yield True, "ap-installation %s" % installation_place, None
|
|
90
|
+
elif diff[Op.REMOVED]:
|
|
91
|
+
yield True, "ap-installation default", None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def wifi_arm(rule, key, diff, root_pre, **_):
|
|
95
|
+
if key[0].startswith("wifi0"):
|
|
96
|
+
prefix, cmd = "wifi0", "a-channel"
|
|
97
|
+
elif key[0].startswith("wifi1"):
|
|
98
|
+
prefix, cmd = "wifi1", "g-channel"
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError("Unknown wifi channel key %r" % key)
|
|
101
|
+
pre_items = list(root_pre.values())[0]["items"]
|
|
102
|
+
unchanged = {k[0]: v[Op.UNCHANGED][0]["row"] for k, v in pre_items.items() if v[Op.UNCHANGED]}
|
|
103
|
+
added = {k[0]: v[Op.ADDED][0]["row"] for k, v in pre_items.items() if v[Op.ADDED]}
|
|
104
|
+
key_arm_channel = prefix + "_arm_channel"
|
|
105
|
+
key_arm_power = prefix + "_arm_power_10x"
|
|
106
|
+
arm_channel, arm_power = "0", "0"
|
|
107
|
+
for params in [unchanged, added]:
|
|
108
|
+
if key_arm_channel in params:
|
|
109
|
+
_, arm_channel = params[key_arm_channel].split(":")
|
|
110
|
+
if key_arm_power in params:
|
|
111
|
+
_, arm_power = params[key_arm_power].split(":")
|
|
112
|
+
yield True, f"{cmd} {arm_channel} {arm_power}", None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def ant_gain(rule, key, diff, root_pre, **_):
|
|
116
|
+
row, value, direct = "", "", None
|
|
117
|
+
if diff[Op.ADDED]:
|
|
118
|
+
row = diff[Op.ADDED][0]["row"]
|
|
119
|
+
_, value = row.split(":")
|
|
120
|
+
direct = True
|
|
121
|
+
elif diff[Op.REMOVED]:
|
|
122
|
+
row = diff[Op.REMOVED][0]["row"]
|
|
123
|
+
value = "0"
|
|
124
|
+
direct = False
|
|
125
|
+
if row:
|
|
126
|
+
if row.startswith("a_"):
|
|
127
|
+
cmd = "a-external-antenna"
|
|
128
|
+
elif row.startswith("g_"):
|
|
129
|
+
cmd = "g-external-antenna"
|
|
130
|
+
else:
|
|
131
|
+
raise ValueError("Unknown row '%s'" % row)
|
|
132
|
+
yield direct, f"{cmd} {value}", None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def ant_pol(rule, key, diff, root_pre, **_):
|
|
136
|
+
row, value, direct = "", "", None
|
|
137
|
+
if diff[Op.ADDED]:
|
|
138
|
+
row, value = diff[Op.ADDED][0]["row"].split(":")
|
|
139
|
+
direct = True
|
|
140
|
+
elif diff[Op.REMOVED]:
|
|
141
|
+
row, _ = diff[Op.REMOVED][0]["row"].split(":")
|
|
142
|
+
value = "0"
|
|
143
|
+
direct = False
|
|
144
|
+
if row:
|
|
145
|
+
cmd = row.replace("_", "-")
|
|
146
|
+
yield direct, f"{cmd} {value}", None
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from annet.annlib.types import Op
|
|
2
|
+
|
|
3
|
+
from annet.rulebook import common
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def diff(old, new, diff_pre, _pops=(Op.AFFECTED,)):
|
|
7
|
+
for iface_row in old:
|
|
8
|
+
_filter_channel_members(old[iface_row])
|
|
9
|
+
for iface_row in new:
|
|
10
|
+
_filter_channel_members(new[iface_row])
|
|
11
|
+
|
|
12
|
+
ret = common.default_diff(old, new, diff_pre, _pops)
|
|
13
|
+
vpn_changed = False
|
|
14
|
+
for (op, cmd, _, _) in ret:
|
|
15
|
+
if op in {Op.ADDED, Op.REMOVED}:
|
|
16
|
+
vpn_changed |= is_vpn_cmd(cmd)
|
|
17
|
+
if vpn_changed:
|
|
18
|
+
for cmd in list(old.keys()):
|
|
19
|
+
if is_ip_cmd(cmd) and not is_vpn_cmd(cmd):
|
|
20
|
+
del old[cmd]
|
|
21
|
+
ret = common.default_diff(old, new, diff_pre, _pops)
|
|
22
|
+
return ret
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_vpn_cmd(cmd):
|
|
26
|
+
return cmd.startswith("vrf member")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_ip_cmd(cmd):
|
|
30
|
+
return cmd.startswith(("ip ", "ipv6 "))
|
|
31
|
+
|
|
32
|
+
# ===
|
|
33
|
+
|
|
34
|
+
# Вырезает все команды не разрешенные
|
|
35
|
+
# на членах агрегата. В running-config
|
|
36
|
+
# листинге они наследуются от самого port-channel
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _filter_channel_members(tree):
|
|
40
|
+
if any(is_in_channel(x) for x in tree):
|
|
41
|
+
for cmd in list(tree.keys()):
|
|
42
|
+
if not _is_allowed_on_channel(cmd):
|
|
43
|
+
del tree[cmd]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_in_channel(cmd_line):
|
|
47
|
+
"""
|
|
48
|
+
Признак того, что это lagg member
|
|
49
|
+
"""
|
|
50
|
+
return cmd_line.startswith("channel-group")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Возможно тут есть еще какие-то команды
|
|
54
|
+
def _is_allowed_on_channel(cmd_line):
|
|
55
|
+
return cmd_line.startswith((
|
|
56
|
+
"channel-group",
|
|
57
|
+
"cdp",
|
|
58
|
+
"description",
|
|
59
|
+
"inherit",
|
|
60
|
+
"ip port",
|
|
61
|
+
"ipv6 port",
|
|
62
|
+
"mac port",
|
|
63
|
+
"lacp",
|
|
64
|
+
"switchport host",
|
|
65
|
+
"shutdown",
|
|
66
|
+
"rate-limit cpu",
|
|
67
|
+
"snmp trap link-status",
|
|
68
|
+
))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
from annet.rulebook import common
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ssh_key(rule, key, diff, hw, **_):
|
|
9
|
+
"""
|
|
10
|
+
При включении ssh надо еще сгенерировать ключ. По конфигу никак не понять есть ли ключ на свитче или нет.
|
|
11
|
+
"""
|
|
12
|
+
if diff[Op.ADDED]:
|
|
13
|
+
added = sorted([x["row"] for x in diff[Op.ADDED]])
|
|
14
|
+
if added == ["ip ssh version 2"]:
|
|
15
|
+
# Отсыпаем mpdaemon-у подсказок для дополнительной команды при наливке
|
|
16
|
+
comment = rule["comment"]
|
|
17
|
+
rule["comment"] = ["!!suppress_errors!!", "!!timeout=240!!"]
|
|
18
|
+
if hw.Cisco.C2960:
|
|
19
|
+
yield (False, "crypto key generate rsa modulus 2048", None)
|
|
20
|
+
else:
|
|
21
|
+
yield (False, "crypto key generate rsa general-keys modulus 2048", None)
|
|
22
|
+
rule["comment"] = comment
|
|
23
|
+
yield from common.default(rule, key, diff)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def no_ipv6_nd_suppress_ra(rule, key, diff, **_):
|
|
27
|
+
"""
|
|
28
|
+
При конфигурации ipv6 nd на нексусах нужно добавлять
|
|
29
|
+
no ipv6 nd suppress-ra
|
|
30
|
+
иначе RA не будет включен.
|
|
31
|
+
К сожалению данной команды не видно в running-config.
|
|
32
|
+
Поэтому подмешиваем ее в патч вместо генератора
|
|
33
|
+
"""
|
|
34
|
+
if diff[Op.ADDED]:
|
|
35
|
+
yield (False, "no ipv6 nd suppress-ra", None)
|
|
36
|
+
yield from common.default(rule, key, diff)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def no_ntp_distribute(rule, key, diff, **_):
|
|
40
|
+
"""
|
|
41
|
+
Для того, чтобы удалить NTP из CFS, сначала нужно сбросить активные
|
|
42
|
+
NTP сессии.
|
|
43
|
+
"""
|
|
44
|
+
if diff[Op.REMOVED]:
|
|
45
|
+
yield (False, "clear ntp session", None)
|
|
46
|
+
yield from common.default(rule, key, diff)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def banner_login(rule, key, diff, **_):
|
|
50
|
+
if diff[Op.REMOVED]:
|
|
51
|
+
yield (False, "no banner login", None)
|
|
52
|
+
elif diff[Op.ADDED]:
|
|
53
|
+
# Убираем дополнительный экранирующий сиимвол
|
|
54
|
+
key = re.sub(r"\^C", "^", key[0])
|
|
55
|
+
yield (False, f"banner login {key}", None)
|
|
56
|
+
else:
|
|
57
|
+
yield from common.default(rule, key, diff)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from annet.annlib.lib import cisco_collapse_vlandb as collapse_vlandb
|
|
4
|
+
from annet.annlib.lib import cisco_expand_vlandb as expand_vlandb
|
|
5
|
+
from annet.annlib.types import Op
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
VLANDB_CHUNK = 15
|
|
9
|
+
SWTRUNK_CHUNK = 5
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# =====
|
|
13
|
+
def simple(rule, key, diff, hw, **_):
|
|
14
|
+
yield from _process_vlandb(rule, key, diff, hw, False, VLANDB_CHUNK)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def swtrunk(rule, key, diff, hw, **_):
|
|
18
|
+
yield from _process_vlandb(rule, key, diff, hw, True, SWTRUNK_CHUNK)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =====
|
|
22
|
+
def _process_vlandb(rule, key, diff, hw, explicit_changing, multi_chunk):
|
|
23
|
+
# pylint: disable=unused-argument
|
|
24
|
+
for affected in diff[Op.AFFECTED]:
|
|
25
|
+
# Изменилось содержимое блока vlan
|
|
26
|
+
yield (True, affected["row"], affected["children"])
|
|
27
|
+
|
|
28
|
+
(prefix, new, new_blocks) = _parse_vlancfg_actions(diff[Op.ADDED])
|
|
29
|
+
(prefix2, old, old_blocks) = _parse_vlancfg_actions(diff[Op.REMOVED])
|
|
30
|
+
if not prefix:
|
|
31
|
+
prefix = prefix2
|
|
32
|
+
|
|
33
|
+
if len(diff[Op.ADDED]) == 1 and len(new) == 0:
|
|
34
|
+
# switchport trunk allowed vlan none
|
|
35
|
+
yield (True, "%s none" % prefix, None)
|
|
36
|
+
return
|
|
37
|
+
for vlan_id in ((set(old_blocks.keys()) - set(new_blocks)) & new):
|
|
38
|
+
# Удалено содержимое блока vlan, но сам влан остался
|
|
39
|
+
yield (True, "%s %s" % (prefix, vlan_id), old_blocks[vlan_id])
|
|
40
|
+
|
|
41
|
+
removed = old.difference(new)
|
|
42
|
+
added = new.difference(old)
|
|
43
|
+
if hw.Catalyst:
|
|
44
|
+
# Каталисты не перечисляют вланы в batch режиме, если они представлены как блоки
|
|
45
|
+
added -= new_blocks.keys()
|
|
46
|
+
|
|
47
|
+
if removed:
|
|
48
|
+
collapsed = collapse_vlandb(removed, hw.Catalyst)
|
|
49
|
+
for chunk in _chunked(collapsed, multi_chunk):
|
|
50
|
+
yield (False, "no %s%s%s" % (prefix, " remove " if explicit_changing else " ", ",".join(chunk)), None)
|
|
51
|
+
|
|
52
|
+
if added:
|
|
53
|
+
collapsed = collapse_vlandb(added, hw.Catalyst)
|
|
54
|
+
for chunk in _chunked(collapsed, multi_chunk):
|
|
55
|
+
yield (True, "%s%s%s" % (prefix, " add " if explicit_changing else " ", ",".join(chunk)), None)
|
|
56
|
+
if new_blocks:
|
|
57
|
+
for vlan_id, block in new_blocks.items():
|
|
58
|
+
yield (True, "%s %s" % (prefix, vlan_id), block)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _chunked(items, size):
|
|
62
|
+
for offset in range(0, len(items), size):
|
|
63
|
+
yield items[offset:offset + size]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_vlancfg_actions(actions):
|
|
67
|
+
prefix = None
|
|
68
|
+
vlandb = set()
|
|
69
|
+
blocks = {}
|
|
70
|
+
for action in actions:
|
|
71
|
+
(prefix, part) = _parse_vlancfg(action["row"])
|
|
72
|
+
if action["children"]:
|
|
73
|
+
assert len(part) == 1, "vlandb block must contain one and only one vlanid: %s" % action["row"]
|
|
74
|
+
blocks[list(part)[0]] = action["children"]
|
|
75
|
+
vlandb.update(part)
|
|
76
|
+
return (prefix, vlandb, blocks)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_vlancfg(row):
|
|
80
|
+
# иногда циски ставят пробелы между влан ренджами, а иногда нет.
|
|
81
|
+
words = re.sub(r",\s+", ",", row).split()
|
|
82
|
+
|
|
83
|
+
if words[-1] == "none":
|
|
84
|
+
# switchport trunk allowed vlan none
|
|
85
|
+
return (" ".join(words[:-1]), set())
|
|
86
|
+
assert re.match(r"[\d,-]+$", words[-1]), "Unable to parse vlancfg row: %s" % row
|
|
87
|
+
prefix = " ".join(words[:-2] if words[-2] == "add" else words[:-1])
|
|
88
|
+
vlancfg = words[-1]
|
|
89
|
+
vlandb = expand_vlandb(vlancfg)
|
|
90
|
+
return (prefix, vlandb)
|
annet/rulebook/common.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
from annet.annlib.rulebook import common # pylint: disable=unused-import # noqa: F401,F403
|
|
5
|
+
from annet.annlib.rulebook.common import * # pylint: disable=wildcard-import,unused-wildcard-import # noqa: F401,F403
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@functools.lru_cache()
|
|
9
|
+
def import_rulebook_function(name):
|
|
10
|
+
from . import rulebook_provider_connector
|
|
11
|
+
|
|
12
|
+
index = name.rindex(".")
|
|
13
|
+
for root in rulebook_provider_connector.get().get_root_modules():
|
|
14
|
+
try:
|
|
15
|
+
module = importlib.import_module(f"{root}.{name[:index]}", package=__name__.rsplit(".", 1)[0])
|
|
16
|
+
return getattr(module, name[index + 1:])
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
19
|
+
raise ImportError(f"Could not import {name}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections import OrderedDict as odict
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
from annet.annlib.rbparser import platform, syntax
|
|
6
|
+
from annet.annlib.rbparser.deploying import compile_messages
|
|
7
|
+
from valkit.common import valid_bool, valid_number, valid_string_list
|
|
8
|
+
from valkit.python import valid_object_path
|
|
9
|
+
|
|
10
|
+
from .common import import_rulebook_function
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Answer = namedtuple("Answer", ("text", "send_nl"))
|
|
14
|
+
DEFAULT_TIMEOUT = 30
|
|
15
|
+
DEFAULT_SEND_NL = True
|
|
16
|
+
DEFAULT_APPLY_LOGIC = "common.apply"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =====
|
|
20
|
+
@functools.lru_cache()
|
|
21
|
+
def compile_deploying_text(text, vendor):
|
|
22
|
+
return _compile_deploying(
|
|
23
|
+
tree=syntax.parse_text(text, params_scheme={
|
|
24
|
+
"timeout": {
|
|
25
|
+
"validator": lambda arg: valid_number(arg, min=1, type=float),
|
|
26
|
+
"default": 30,
|
|
27
|
+
},
|
|
28
|
+
"send_nl": {
|
|
29
|
+
"validator": valid_bool,
|
|
30
|
+
"default": True,
|
|
31
|
+
},
|
|
32
|
+
"apply_logic": {
|
|
33
|
+
"validator": valid_object_path,
|
|
34
|
+
"default": DEFAULT_APPLY_LOGIC,
|
|
35
|
+
},
|
|
36
|
+
"ifcontext": {
|
|
37
|
+
"validator": valid_string_list,
|
|
38
|
+
"default": [],
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
reverse_prefix=platform.VENDOR_REVERSES[vendor],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =====
|
|
46
|
+
def _compile_deploying(tree, reverse_prefix):
|
|
47
|
+
deploying = odict()
|
|
48
|
+
for (rule_id, attrs) in tree.items():
|
|
49
|
+
if attrs["type"] == "normal" and not attrs["row"].startswith(("ignore:", "dialog:")):
|
|
50
|
+
(ignore, dialogs) = compile_messages(attrs["children"])
|
|
51
|
+
deploying[rule_id] = {
|
|
52
|
+
"attrs": {
|
|
53
|
+
"regexp": syntax.compile_row_regexp(attrs["row"]),
|
|
54
|
+
"timeout": attrs["params"]["timeout"],
|
|
55
|
+
"apply_logic": import_rulebook_function(attrs["params"]["apply_logic"]),
|
|
56
|
+
"ignore": ignore,
|
|
57
|
+
"dialogs": dialogs,
|
|
58
|
+
"ifcontext": attrs["params"]["ifcontext"],
|
|
59
|
+
},
|
|
60
|
+
"children": _compile_deploying(attrs["children"], reverse_prefix),
|
|
61
|
+
}
|
|
62
|
+
return deploying
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def match_deploy_rule(rules, cmd_path, context):
|
|
66
|
+
for (depth, row) in enumerate(cmd_path):
|
|
67
|
+
for rule in rules.values():
|
|
68
|
+
if rule["attrs"]["regexp"].match(row):
|
|
69
|
+
ifcontext = rule["attrs"]["ifcontext"]
|
|
70
|
+
if syntax.match_context(ifcontext, context):
|
|
71
|
+
if depth == len(cmd_path) - 1:
|
|
72
|
+
return rule
|
|
73
|
+
else:
|
|
74
|
+
rules = rule["children"]
|
|
75
|
+
if len(rules) == 0:
|
|
76
|
+
break
|
|
77
|
+
# default match
|
|
78
|
+
return {
|
|
79
|
+
"attrs": {
|
|
80
|
+
"regexp": syntax.compile_row_regexp("~"),
|
|
81
|
+
"timeout": DEFAULT_TIMEOUT,
|
|
82
|
+
"apply_logic": import_rulebook_function(DEFAULT_APPLY_LOGIC),
|
|
83
|
+
"ignore": [],
|
|
84
|
+
"dialogs": odict(),
|
|
85
|
+
},
|
|
86
|
+
"children": odict(),
|
|
87
|
+
}
|
|
File without changes
|