annet 0.0__tar.gz
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-0.0/AUTHORS +21 -0
- annet-0.0/LICENSE +21 -0
- annet-0.0/MANIFEST.in +1 -0
- annet-0.0/PKG-INFO +25 -0
- annet-0.0/README.md +111 -0
- annet-0.0/annet/__init__.py +61 -0
- annet-0.0/annet/adapters/__init__.py +0 -0
- annet-0.0/annet/adapters/netbox/__init__.py +0 -0
- annet-0.0/annet/adapters/netbox/common/__init__.py +0 -0
- annet-0.0/annet/adapters/netbox/common/client.py +87 -0
- annet-0.0/annet/adapters/netbox/common/manufacturer.py +62 -0
- annet-0.0/annet/adapters/netbox/common/models.py +105 -0
- annet-0.0/annet/adapters/netbox/common/query.py +23 -0
- annet-0.0/annet/adapters/netbox/common/status_client.py +25 -0
- annet-0.0/annet/adapters/netbox/common/storage_opts.py +14 -0
- annet-0.0/annet/adapters/netbox/provider.py +34 -0
- annet-0.0/annet/adapters/netbox/v24/__init__.py +0 -0
- annet-0.0/annet/adapters/netbox/v24/api_models.py +73 -0
- annet-0.0/annet/adapters/netbox/v24/client.py +59 -0
- annet-0.0/annet/adapters/netbox/v24/storage.py +196 -0
- annet-0.0/annet/adapters/netbox/v37/__init__.py +0 -0
- annet-0.0/annet/adapters/netbox/v37/api_models.py +38 -0
- annet-0.0/annet/adapters/netbox/v37/client.py +62 -0
- annet-0.0/annet/adapters/netbox/v37/storage.py +149 -0
- annet-0.0/annet/annet.py +25 -0
- annet-0.0/annet/annlib/__init__.py +7 -0
- annet-0.0/annet/annlib/command.py +49 -0
- annet-0.0/annet/annlib/diff.py +158 -0
- annet-0.0/annet/annlib/errors.py +8 -0
- annet-0.0/annet/annlib/filter_acl.py +196 -0
- annet-0.0/annet/annlib/jsontools.py +116 -0
- annet-0.0/annet/annlib/lib.py +495 -0
- annet-0.0/annet/annlib/netdev/__init__.py +0 -0
- annet-0.0/annet/annlib/netdev/db.py +62 -0
- annet-0.0/annet/annlib/netdev/devdb/__init__.py +28 -0
- annet-0.0/annet/annlib/netdev/devdb/data/devdb.json +137 -0
- annet-0.0/annet/annlib/netdev/views/__init__.py +0 -0
- annet-0.0/annet/annlib/netdev/views/dump.py +121 -0
- annet-0.0/annet/annlib/netdev/views/hardware.py +112 -0
- annet-0.0/annet/annlib/output.py +246 -0
- annet-0.0/annet/annlib/patching.py +533 -0
- annet-0.0/annet/annlib/rbparser/__init__.py +0 -0
- annet-0.0/annet/annlib/rbparser/acl.py +120 -0
- annet-0.0/annet/annlib/rbparser/deploying.py +55 -0
- annet-0.0/annet/annlib/rbparser/ordering.py +52 -0
- annet-0.0/annet/annlib/rbparser/platform.py +51 -0
- annet-0.0/annet/annlib/rbparser/syntax.py +115 -0
- annet-0.0/annet/annlib/rulebook/__init__.py +0 -0
- annet-0.0/annet/annlib/rulebook/common.py +350 -0
- annet-0.0/annet/annlib/tabparser.py +648 -0
- annet-0.0/annet/annlib/types.py +35 -0
- annet-0.0/annet/api/__init__.py +826 -0
- annet-0.0/annet/argparse.py +415 -0
- annet-0.0/annet/cli.py +237 -0
- annet-0.0/annet/cli_args.py +503 -0
- annet-0.0/annet/configs/context.yml +18 -0
- annet-0.0/annet/configs/logging.yaml +39 -0
- annet-0.0/annet/connectors.py +77 -0
- annet-0.0/annet/deploy.py +536 -0
- annet-0.0/annet/diff.py +84 -0
- annet-0.0/annet/executor.py +551 -0
- annet-0.0/annet/filtering.py +40 -0
- annet-0.0/annet/gen.py +865 -0
- annet-0.0/annet/generators/__init__.py +435 -0
- annet-0.0/annet/generators/base.py +136 -0
- annet-0.0/annet/generators/common/__init__.py +0 -0
- annet-0.0/annet/generators/common/initial.py +33 -0
- annet-0.0/annet/generators/entire.py +97 -0
- annet-0.0/annet/generators/exceptions.py +10 -0
- annet-0.0/annet/generators/jsonfragment.py +125 -0
- annet-0.0/annet/generators/partial.py +119 -0
- annet-0.0/annet/generators/perf.py +79 -0
- annet-0.0/annet/generators/ref.py +15 -0
- annet-0.0/annet/generators/result.py +127 -0
- annet-0.0/annet/hardware.py +45 -0
- annet-0.0/annet/implicit.py +139 -0
- annet-0.0/annet/lib.py +128 -0
- annet-0.0/annet/output.py +167 -0
- annet-0.0/annet/parallel.py +448 -0
- annet-0.0/annet/patching.py +25 -0
- annet-0.0/annet/reference.py +148 -0
- annet-0.0/annet/rulebook/__init__.py +114 -0
- annet-0.0/annet/rulebook/arista/__init__.py +0 -0
- annet-0.0/annet/rulebook/arista/iface.py +16 -0
- annet-0.0/annet/rulebook/aruba/__init__.py +16 -0
- annet-0.0/annet/rulebook/aruba/ap_env.py +146 -0
- annet-0.0/annet/rulebook/aruba/misc.py +8 -0
- annet-0.0/annet/rulebook/cisco/__init__.py +0 -0
- annet-0.0/annet/rulebook/cisco/iface.py +68 -0
- annet-0.0/annet/rulebook/cisco/misc.py +57 -0
- annet-0.0/annet/rulebook/cisco/vlandb.py +90 -0
- annet-0.0/annet/rulebook/common.py +19 -0
- annet-0.0/annet/rulebook/deploying.py +87 -0
- annet-0.0/annet/rulebook/huawei/__init__.py +0 -0
- annet-0.0/annet/rulebook/huawei/aaa.py +75 -0
- annet-0.0/annet/rulebook/huawei/bgp.py +97 -0
- annet-0.0/annet/rulebook/huawei/iface.py +33 -0
- annet-0.0/annet/rulebook/huawei/misc.py +337 -0
- annet-0.0/annet/rulebook/huawei/vlandb.py +115 -0
- annet-0.0/annet/rulebook/juniper/__init__.py +107 -0
- annet-0.0/annet/rulebook/nexus/__init__.py +0 -0
- annet-0.0/annet/rulebook/nexus/iface.py +92 -0
- annet-0.0/annet/rulebook/patching.py +143 -0
- annet-0.0/annet/rulebook/ribbon/__init__.py +12 -0
- annet-0.0/annet/rulebook/texts/arista.deploy +20 -0
- annet-0.0/annet/rulebook/texts/arista.order +125 -0
- annet-0.0/annet/rulebook/texts/arista.rul +59 -0
- annet-0.0/annet/rulebook/texts/aruba.deploy +20 -0
- annet-0.0/annet/rulebook/texts/aruba.order +83 -0
- annet-0.0/annet/rulebook/texts/aruba.rul +87 -0
- annet-0.0/annet/rulebook/texts/cisco.deploy +27 -0
- annet-0.0/annet/rulebook/texts/cisco.order +82 -0
- annet-0.0/annet/rulebook/texts/cisco.rul +105 -0
- annet-0.0/annet/rulebook/texts/huawei.deploy +188 -0
- annet-0.0/annet/rulebook/texts/huawei.order +388 -0
- annet-0.0/annet/rulebook/texts/huawei.rul +471 -0
- annet-0.0/annet/rulebook/texts/juniper.rul +120 -0
- annet-0.0/annet/rulebook/texts/nexus.deploy +24 -0
- annet-0.0/annet/rulebook/texts/nexus.order +85 -0
- annet-0.0/annet/rulebook/texts/nexus.rul +83 -0
- annet-0.0/annet/rulebook/texts/nokia.rul +31 -0
- annet-0.0/annet/rulebook/texts/pc.order +5 -0
- annet-0.0/annet/rulebook/texts/pc.rul +9 -0
- annet-0.0/annet/rulebook/texts/ribbon.deploy +22 -0
- annet-0.0/annet/rulebook/texts/ribbon.rul +77 -0
- annet-0.0/annet/rulebook/texts/routeros.order +38 -0
- annet-0.0/annet/rulebook/texts/routeros.rul +45 -0
- annet-0.0/annet/storage.py +125 -0
- annet-0.0/annet/tabparser.py +36 -0
- annet-0.0/annet/text_term_format.py +95 -0
- annet-0.0/annet/tracing.py +170 -0
- annet-0.0/annet/types.py +227 -0
- annet-0.0/annet.egg-info/PKG-INFO +25 -0
- annet-0.0/annet.egg-info/SOURCES.txt +142 -0
- annet-0.0/annet.egg-info/dependency_links.txt +1 -0
- annet-0.0/annet.egg-info/entry_points.txt +5 -0
- annet-0.0/annet.egg-info/requires.txt +16 -0
- annet-0.0/annet.egg-info/top_level.txt +2 -0
- annet-0.0/annet_generators/__init__.py +0 -0
- annet-0.0/annet_generators/example/__init__.py +12 -0
- annet-0.0/annet_generators/example/lldp.py +53 -0
- annet-0.0/requirements.txt +16 -0
- annet-0.0/setup.cfg +4 -0
- annet-0.0/setup.py +46 -0
annet-0.0/AUTHORS
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The following authors have created the source code of "annet" published and distributed by YANDEX LLC as the owner:
|
|
2
|
+
|
|
3
|
+
Aleksandr Balezin <gescheit@yandex-team.ru>
|
|
4
|
+
Aleksandr Klimenko <v0lk@yandex-team.ru>
|
|
5
|
+
Aleksei Lymar <alymar@nebius.com>
|
|
6
|
+
Alexey Andriyanov <alan@yandex-team.ru>
|
|
7
|
+
Alexey Esin <adess@yandex-team.ru>
|
|
8
|
+
Anton Egorov <eg0rov@yandex-team.ru>
|
|
9
|
+
Artem Denisov <artemdenisov@yandex-team.ru>
|
|
10
|
+
Azat Kurbanov <azatkurbanov@yandex-team.ru>
|
|
11
|
+
Devaev Maxim <mdevaev@yandex-team.ru>
|
|
12
|
+
Fedor Zhukov <azryve@nebius.com>
|
|
13
|
+
Grigorii Ozhegov <ozhegov@yandex-team.ru>
|
|
14
|
+
Grigorii Solovev <gslv@yandex-team.ru>
|
|
15
|
+
Konstantin Sazonov <moonug@yandex-team.ru>
|
|
16
|
+
Roman Glebov <kitaro@yandex-team.ru>
|
|
17
|
+
Roman Karaulanov <anteron92@yandex-team.ru>
|
|
18
|
+
Sergey Mishchenko <smishche@yandex-team.ru>
|
|
19
|
+
Vlad Starostin <vladstar@yandex-team.ru>
|
|
20
|
+
Vladimir Sukhonosov <xornet@yandex-team.ru>
|
|
21
|
+
Vladislav Daniliuk <jayt@nebius.com>
|
annet-0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2023] YANDEX LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
annet-0.0/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include requirements.txt
|
annet-0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: annet
|
|
3
|
+
Version: 0.0
|
|
4
|
+
Summary: annet
|
|
5
|
+
Home-page: https://github.com/annetutil/annet
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: AUTHORS
|
|
10
|
+
Requires-Dist: colorama>=0.4.6
|
|
11
|
+
Requires-Dist: tabulate>=0.9.0
|
|
12
|
+
Requires-Dist: jsonpatch>=1.33
|
|
13
|
+
Requires-Dist: jsonpointer>=2.4
|
|
14
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
15
|
+
Requires-Dist: Pygments>=2.14.0
|
|
16
|
+
Requires-Dist: Mako>=1.2.4
|
|
17
|
+
Requires-Dist: Jinja2>=3.1.2
|
|
18
|
+
Requires-Dist: psutil>=5.8.0
|
|
19
|
+
Requires-Dist: packaging>=23.2
|
|
20
|
+
Requires-Dist: contextlog>=1.1
|
|
21
|
+
Requires-Dist: valkit>=0.1.4
|
|
22
|
+
Requires-Dist: aiohttp>=3.8.4
|
|
23
|
+
Requires-Dist: yarl>=1.8.2
|
|
24
|
+
Requires-Dist: adaptix==3.0.0b2
|
|
25
|
+
Requires-Dist: dataclass-rest==0.4
|
annet-0.0/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Annet - network configuration utility
|
|
2
|
+
|
|
3
|
+
The system contains network appliance config generators written in Python with optional use of text preprocessors (Jinja2, Mako).
|
|
4
|
+
Huawei, Cisco IOS, Cisco NX-OS, Cisco IOS-XR, Juniper, as well as devices configured via separate config files (Linux, FreeBSD, Cumulus) are supported.
|
|
5
|
+
|
|
6
|
+
annet has a number of modes (subcommands):
|
|
7
|
+
|
|
8
|
+
- ```annet gen``` - generates the entire config for the specified devices or specified parts of it
|
|
9
|
+
- ```annet diff``` - first does gen and then builds diff with current config version
|
|
10
|
+
- ```annet patch``` - first does diff and then generates a list of commands to apply diff on the device
|
|
11
|
+
|
|
12
|
+
Usage help can be obtained by calling ```annet -h``` or for a specific command, such as ```annet gen -h```.
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Provide `NETBOX_URL` and `NETBOX_TOKEN` environment variable to setup data source.
|
|
19
|
+
|
|
20
|
+
```shell
|
|
21
|
+
export NETBOX_URL="https://demo.netbox.dev"
|
|
22
|
+
export NETBOX_TOKEN="1234567890abcdef01234567890abcdef0123456"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### annet gen
|
|
26
|
+
|
|
27
|
+
The annet_generators directory contains many files called generators.
|
|
28
|
+
A generator takes information about the switch as input and returns the configuration.
|
|
29
|
+
The part of the config that the generator is responsible for is specified in the generator's acl function. If a generator returns a configuration that does not fall under acl, an exception will be thrown.
|
|
30
|
+
|
|
31
|
+
Example generator:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from annet.generators import PartialGenerator
|
|
35
|
+
|
|
36
|
+
class Mtu(PartialGenerator):
|
|
37
|
+
TAGS = ["mtu"]
|
|
38
|
+
def acl_cisco(self, _):
|
|
39
|
+
return "system mtu jumbo"
|
|
40
|
+
|
|
41
|
+
def run_cisco(self, device):
|
|
42
|
+
yield "system mtu jumbo %d" % 9000
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
And an example of calling annet:
|
|
47
|
+
```bash
|
|
48
|
+
annet gen -g mtu sw6-i1
|
|
49
|
+
# -------------------- sw6-i1.cfg --------------------
|
|
50
|
+
system mtu jumbo 9000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Method `acl_cisco` defines scope of the generator, which commands and block it controls.
|
|
54
|
+
The option `-g mtu` means that only generators with the mtu element in the TAGS variable should be called. If no tag is specified, all generators will be executed.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### annet diff
|
|
58
|
+
|
|
59
|
+
If we were configuring the switch from scratch, these options would be enough, but in our reality we need to be able not only to generate the desired configuration, but also to be able to bring the current configuration to the desired one.
|
|
60
|
+
To do this, you need to be able to delete an outdated configuration and correctly add a new one. The **diff** module, which implements some tricky logic, is responsible for this work.
|
|
61
|
+
This logic is defined in the rulebook/texts/VENDOR folder.
|
|
62
|
+
|
|
63
|
+
Example diff:
|
|
64
|
+
```diff
|
|
65
|
+
# -------------------- sw1-i38.cfg --------------------
|
|
66
|
+
acl number 2610
|
|
67
|
+
- rule 40 permit source 10.11.170.150 0
|
|
68
|
+
+ rule 12 permit source 10.11.133.81 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### annet patch
|
|
72
|
+
|
|
73
|
+
Next, you need to create a list of commands from the resulting diff. The **patch** module is responsible for this. It receives the diff, runs the logic specified in rulebook/texts/VENDOR and returns the list of commands.
|
|
74
|
+
Let's take the above diff. It says to remove the command ``rule 40 permit source 10.11.170.150 0`` and add ``rule 12 permit source 10.11.133.81 0``.
|
|
75
|
+
Basic command delete logic for huawei is adding undo to the command. So the undo command will look like this: ``undo rule 40 permit source 10.11.170.150 0```, but this is an invalid command. In case of canceling acl rules, you need to execute ``undo rule N```.
|
|
76
|
+
So you need to write the undo logic for the ```rule ```` command in the ``acl ``` block.
|
|
77
|
+
Here is the part of rulebook/texts/huawei.rul responsible for this:
|
|
78
|
+
```
|
|
79
|
+
acl name *
|
|
80
|
+
rule * %logic=huawei.misc.undo_redo
|
|
81
|
+
```
|
|
82
|
+
The asterisk here means that the key argument of the undo_redo function will contain the first word after rule, namely the rule number.
|
|
83
|
+
|
|
84
|
+
Here, the undo_redo function from the file in rulebook/huawei/misc.py is used to generate the command to remove rules in acl.
|
|
85
|
+
```python
|
|
86
|
+
def undo_redo(rule, key, diff, **_):
|
|
87
|
+
...
|
|
88
|
+
```
|
|
89
|
+
Now calling ```annet patch -g snmp sw1-i38```` returns the correct set of commands.
|
|
90
|
+
```
|
|
91
|
+
acl number 2610
|
|
92
|
+
undo rule 40
|
|
93
|
+
rule 12 permit source 10.11.133.81 0
|
|
94
|
+
quit
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
<!-- ### annet deploy
|
|
99
|
+
|
|
100
|
+
To apply these commands on a switch there is a **deploy** module.
|
|
101
|
+
annet can apply changes (roll out) to multiple devices at the same time.
|
|
102
|
+
|
|
103
|
+
By default, the edits that annet proposes to roll out will be shown before the rollout.
|
|
104
|
+
The user must confirm that they agree to roll out the proposed diff to a given list of devices.
|
|
105
|
+
During the rollout, annet will display the overall progress of the task and the log of one of the devices.
|
|
106
|
+
|
|
107
|
+
Normal layout. The screen with patches will be shown and the process of laying out will be displayed.
|
|
108
|
+
```bash
|
|
109
|
+
annet deploy -g snmp $HOST
|
|
110
|
+
```
|
|
111
|
+
Credentials will be used from the current user (username, ssh key, ssh agent, encrypted password in $HOME). -->
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
import os
|
|
4
|
+
import pkgutil
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import SUPPRESS, Namespace
|
|
7
|
+
|
|
8
|
+
import colorama
|
|
9
|
+
import yaml
|
|
10
|
+
from annet.annlib.errors import ( # pylint: disable=wrong-import-position
|
|
11
|
+
DeployCancelled,
|
|
12
|
+
ExecError,
|
|
13
|
+
)
|
|
14
|
+
from contextlog import patch_logging, patch_threading
|
|
15
|
+
from valkit.python import valid_logging_level
|
|
16
|
+
|
|
17
|
+
import annet.argparse
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("DeployCancelled", "ExecError")
|
|
21
|
+
|
|
22
|
+
DEBUG2_LEVELV_NUM = 9
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fill_base_args(parser: annet.argparse.ArgParser, pkg_name: str, logging_config: str):
|
|
26
|
+
parser.add_argument("--log-level", default="WARN", type=valid_logging_level,
|
|
27
|
+
help="Уровень детализации логов (DEBUG, DEBUG2 (with comocutor debug), INFO, WARN, CRITICAL)")
|
|
28
|
+
parser.add_argument("--pkg_name", default=pkg_name, help=SUPPRESS)
|
|
29
|
+
parser.add_argument("--logging_config", default=logging_config, help=SUPPRESS)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init_logging(options: Namespace):
|
|
33
|
+
patch_logging()
|
|
34
|
+
patch_threading()
|
|
35
|
+
logging.captureWarnings(True)
|
|
36
|
+
logging_config = yaml.safe_load(pkgutil.get_data(options.pkg_name, options.logging_config))
|
|
37
|
+
if options.log_level is not None:
|
|
38
|
+
logging_config.setdefault("root", {})
|
|
39
|
+
logging_config["root"]["level"] = options.log_level
|
|
40
|
+
logging.addLevelName(DEBUG2_LEVELV_NUM, "DEBUG2")
|
|
41
|
+
logging.config.dictConfig(logging_config)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def init(options: Namespace):
|
|
45
|
+
init_logging(options)
|
|
46
|
+
|
|
47
|
+
# Отключить colorama.init, если стоит env-переменная. Нужно в тестах
|
|
48
|
+
if os.environ.get("ANN_FORCE_COLOR", None) not in [None, "", "0", "no"]:
|
|
49
|
+
colorama.init = lambda *_, **__: None
|
|
50
|
+
colorama.init()
|
|
51
|
+
|
|
52
|
+
# Workaround for Python 3.8.0: https://bugs.python.org/issue38529
|
|
53
|
+
import asyncio.streams
|
|
54
|
+
if hasattr(asyncio.streams.StreamReaderProtocol, "_on_reader_gc"):
|
|
55
|
+
asyncio.streams.StreamReaderProtocol._on_reader_gc = lambda *args, **kwargs: None # pylint: disable=protected-access
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def assert_python_version():
|
|
59
|
+
if sys.version_info < (3, 8, 0):
|
|
60
|
+
sys.stderr.write("Error: you need python 3.8.0 or higher\n")
|
|
61
|
+
sys.exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Generic, Optional, List, TypeVar, Callable
|
|
4
|
+
|
|
5
|
+
from dataclass_rest.http.requests import RequestsClient
|
|
6
|
+
from requests import Session
|
|
7
|
+
|
|
8
|
+
Model = TypeVar("Model")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PagingResponse(Generic[Model]):
|
|
13
|
+
next: Optional[str]
|
|
14
|
+
previous: Optional[str]
|
|
15
|
+
count: int
|
|
16
|
+
results: List[Model]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Func = TypeVar("Func", bound=Callable)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _collect_by_pages(func: Func) -> Func:
|
|
23
|
+
"""Collect all results using only pagination."""
|
|
24
|
+
@wraps(func)
|
|
25
|
+
def wrapper(self, *args, **kwargs):
|
|
26
|
+
kwargs.setdefault("offset", 0)
|
|
27
|
+
limit = kwargs.setdefault("limit", 100)
|
|
28
|
+
results = []
|
|
29
|
+
method = func.__get__(self, self.__class__)
|
|
30
|
+
has_next = True
|
|
31
|
+
while has_next:
|
|
32
|
+
page = method(*args, **kwargs)
|
|
33
|
+
kwargs["offset"] += limit
|
|
34
|
+
results.extend(page.results)
|
|
35
|
+
has_next = bool(page.next)
|
|
36
|
+
return PagingResponse(
|
|
37
|
+
previous=None,
|
|
38
|
+
next=None,
|
|
39
|
+
count=len(results),
|
|
40
|
+
results=results,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return wrapper
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# default batch size 100 is calculated to fit list of UUIDs in 4k URL length
|
|
47
|
+
def collect(func: Func, field: str = "", batch_size: int = 100) -> Func:
|
|
48
|
+
"""
|
|
49
|
+
Collect data from method iterating over pages and filter batches.
|
|
50
|
+
|
|
51
|
+
:param func: Method to call
|
|
52
|
+
:param field: Field which defines a filter split into batches
|
|
53
|
+
:param batch_size: Limit of values in `field` filter requested at a time
|
|
54
|
+
"""
|
|
55
|
+
func = _collect_by_pages(func)
|
|
56
|
+
if not field:
|
|
57
|
+
return func
|
|
58
|
+
|
|
59
|
+
@wraps(func)
|
|
60
|
+
def wrapper(self, *args, **kwargs):
|
|
61
|
+
value = kwargs.get(field)
|
|
62
|
+
if not value:
|
|
63
|
+
return func(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
method = func.__get__(self, self.__class__)
|
|
66
|
+
results = []
|
|
67
|
+
for offset in range(0, len(value), batch_size):
|
|
68
|
+
kwargs[field] = value[offset:offset + batch_size]
|
|
69
|
+
page = method(*args, **kwargs)
|
|
70
|
+
results.extend(page.results)
|
|
71
|
+
return PagingResponse(
|
|
72
|
+
previous=None,
|
|
73
|
+
next=None,
|
|
74
|
+
count=len(results),
|
|
75
|
+
results=results,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return wrapper
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class BaseNetboxClient(RequestsClient):
|
|
82
|
+
def __init__(self, url: str, token: str):
|
|
83
|
+
url = url.rstrip("/") + "/api/"
|
|
84
|
+
session = Session()
|
|
85
|
+
if token:
|
|
86
|
+
session.headers["Authorization"] = f"Token {token}"
|
|
87
|
+
super().__init__(url, session)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
|
|
3
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
4
|
+
|
|
5
|
+
logger = getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
_VENDORS = {
|
|
8
|
+
"cisco": "Cisco",
|
|
9
|
+
"catalyst": "Cisco Catalyst",
|
|
10
|
+
"nexus": "Cisco Nexus",
|
|
11
|
+
"huawei": "Huawei",
|
|
12
|
+
"juniper": "Juniper",
|
|
13
|
+
"arista": "Arista",
|
|
14
|
+
"pc": "PC",
|
|
15
|
+
"nokia": "Nokia",
|
|
16
|
+
"aruba": "Aruba",
|
|
17
|
+
"routeros": "RouterOS",
|
|
18
|
+
"ribbon": "Ribbon",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _vendor_to_hw(vendor):
|
|
23
|
+
return HardwareView(_VENDORS.get(vendor.lower(), vendor), None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_hw(manufacturer: str, model: str):
|
|
27
|
+
# by some reason Netbox calls Mellanox SN as MSN, so we fix them here
|
|
28
|
+
if manufacturer == "Mellanox" and model.startswith("MSN"):
|
|
29
|
+
model = model.replace("MSN", "SN", 1)
|
|
30
|
+
hw = _vendor_to_hw(manufacturer + " " + model)
|
|
31
|
+
if not hw:
|
|
32
|
+
raise ValueError(f"unsupported manufacturer {manufacturer}")
|
|
33
|
+
return hw
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_breed(manufacturer: str, model: str):
|
|
37
|
+
if manufacturer == "Huawei" and model.startswith("CE"):
|
|
38
|
+
return "vrp85"
|
|
39
|
+
elif manufacturer == "Huawei" and model.startswith("NE"):
|
|
40
|
+
return "vrp85"
|
|
41
|
+
elif manufacturer == "Huawei":
|
|
42
|
+
return "vrp55"
|
|
43
|
+
elif manufacturer == "Mellanox":
|
|
44
|
+
return "cuml2"
|
|
45
|
+
elif manufacturer == "Juniper":
|
|
46
|
+
return "jun10"
|
|
47
|
+
elif manufacturer == "Cisco":
|
|
48
|
+
return "ios12"
|
|
49
|
+
elif manufacturer == "Adva":
|
|
50
|
+
return "adva8"
|
|
51
|
+
elif manufacturer == "Arista":
|
|
52
|
+
return "eos4"
|
|
53
|
+
raise ValueError(f"unsupported manufacturer {manufacturer}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_supported(manufacturer: str) -> bool:
|
|
57
|
+
if manufacturer not in (
|
|
58
|
+
"Huawei", "Mellanox", "Juniper", "Cisco", "Adva", "Arista",
|
|
59
|
+
):
|
|
60
|
+
logger.warning("Unsupported manufacturer `%s`", manufacturer)
|
|
61
|
+
return False
|
|
62
|
+
return True
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
6
|
+
from annet.storage import Storage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Entity:
|
|
11
|
+
id: int
|
|
12
|
+
name: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Label:
|
|
17
|
+
value: str
|
|
18
|
+
label: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class IpFamily:
|
|
23
|
+
value: int
|
|
24
|
+
label: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class DeviceType:
|
|
29
|
+
id: int
|
|
30
|
+
manufacturer: Entity
|
|
31
|
+
model: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DeviceIp:
|
|
36
|
+
id: int
|
|
37
|
+
display: str
|
|
38
|
+
address: str
|
|
39
|
+
family: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class IpAddress:
|
|
44
|
+
id: int
|
|
45
|
+
assigned_object_id: int
|
|
46
|
+
display: str
|
|
47
|
+
family: IpFamily
|
|
48
|
+
address: str
|
|
49
|
+
status: Label
|
|
50
|
+
tags: List[Entity]
|
|
51
|
+
created: datetime
|
|
52
|
+
last_updated: datetime
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Interface(Entity):
|
|
57
|
+
device: Entity
|
|
58
|
+
enabled: bool
|
|
59
|
+
display: str = ""
|
|
60
|
+
ip_addresses: List[IpAddress] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class NetboxDevice(Entity):
|
|
65
|
+
url: str
|
|
66
|
+
storage: Storage
|
|
67
|
+
neighbours_ids: List[int]
|
|
68
|
+
|
|
69
|
+
display: str
|
|
70
|
+
device_type: DeviceType
|
|
71
|
+
device_role: Entity
|
|
72
|
+
tenant: Optional[Entity]
|
|
73
|
+
platform: Optional[Entity]
|
|
74
|
+
serial: str
|
|
75
|
+
asset_tag: Optional[str]
|
|
76
|
+
site: Entity
|
|
77
|
+
rack: Optional[Entity]
|
|
78
|
+
position: Optional[float]
|
|
79
|
+
face: Optional[Label]
|
|
80
|
+
status: Label
|
|
81
|
+
primary_ip: Optional[DeviceIp]
|
|
82
|
+
primary_ip4: Optional[DeviceIp]
|
|
83
|
+
primary_ip6: Optional[DeviceIp]
|
|
84
|
+
tags: List[Entity]
|
|
85
|
+
custom_fields: Dict[str, Any]
|
|
86
|
+
created: datetime
|
|
87
|
+
last_updated: datetime
|
|
88
|
+
|
|
89
|
+
fqdn: str
|
|
90
|
+
hostname: str
|
|
91
|
+
hw: Optional[HardwareView]
|
|
92
|
+
breed: str
|
|
93
|
+
|
|
94
|
+
interfaces: List[Interface]
|
|
95
|
+
|
|
96
|
+
# compat
|
|
97
|
+
|
|
98
|
+
def __hash__(self):
|
|
99
|
+
return hash((self.id, type(self)))
|
|
100
|
+
|
|
101
|
+
def __eq__(self, other):
|
|
102
|
+
return type(self) is type(other) and self.url == other.url
|
|
103
|
+
|
|
104
|
+
def is_pc(self):
|
|
105
|
+
return self.device_type.manufacturer.name == "Mellanox"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Union, Iterable, Optional
|
|
3
|
+
|
|
4
|
+
from annet.storage import Query
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class NetboxQuery(Query):
|
|
9
|
+
query: List[str]
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def new(
|
|
13
|
+
cls, query: Union[str, Iterable[str]],
|
|
14
|
+
hosts_range: Optional[slice] = None,
|
|
15
|
+
) -> "NetboxQuery":
|
|
16
|
+
if hosts_range is not None:
|
|
17
|
+
raise ValueError("host_range is not supported")
|
|
18
|
+
return cls(query=list(query))
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def globs(self):
|
|
22
|
+
# We process every query host as a glob
|
|
23
|
+
return self.query
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from adaptix import Retort, name_mapping, NameStyle
|
|
5
|
+
from dataclass_rest import get
|
|
6
|
+
from dataclass_rest.client_protocol import FactoryProtocol
|
|
7
|
+
|
|
8
|
+
from .client import BaseNetboxClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Status:
|
|
13
|
+
netbox_version: str
|
|
14
|
+
plugins: Dict[str, str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NetboxStatusClient(BaseNetboxClient):
|
|
18
|
+
def _init_response_body_factory(self) -> FactoryProtocol:
|
|
19
|
+
return Retort(recipe=[
|
|
20
|
+
name_mapping(name_style=NameStyle.LOWER_KEBAB)
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
@get("status")
|
|
24
|
+
def status(self) -> Status:
|
|
25
|
+
...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NetboxStorageOpts:
|
|
5
|
+
def __init__(self, url: str, token: str):
|
|
6
|
+
self.url = url
|
|
7
|
+
self.token = token
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_cli_opts(cls, cli_opts):
|
|
11
|
+
return cls(
|
|
12
|
+
url=os.getenv("NETBOX_URL", "http://localhost"),
|
|
13
|
+
token=os.getenv("NETBOX_TOKEN", "").strip(),
|
|
14
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclass_rest.exceptions import ClientError
|
|
2
|
+
|
|
3
|
+
from annet.storage import StorageProvider, Storage
|
|
4
|
+
from .common.status_client import NetboxStatusClient
|
|
5
|
+
from .common.storage_opts import NetboxStorageOpts
|
|
6
|
+
from .common.query import NetboxQuery
|
|
7
|
+
from .v24.storage import NetboxStorageV24
|
|
8
|
+
from .v37.storage import NetboxStorageV37
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def storage_factory(opts: NetboxStorageOpts) -> Storage:
|
|
12
|
+
client = NetboxStatusClient(opts.url, opts.token)
|
|
13
|
+
try:
|
|
14
|
+
status = client.status()
|
|
15
|
+
except ClientError as e:
|
|
16
|
+
if e.status_code == 404:
|
|
17
|
+
# old version do not support status reqeust
|
|
18
|
+
return NetboxStorageV24(opts)
|
|
19
|
+
raise
|
|
20
|
+
if status.netbox_version.startswith("3."):
|
|
21
|
+
return NetboxStorageV37(opts)
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError(f"Unsupported version: {status.netbox_version}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NetboxProvider(StorageProvider):
|
|
27
|
+
def storage(self):
|
|
28
|
+
return storage_factory
|
|
29
|
+
|
|
30
|
+
def opts(self):
|
|
31
|
+
return NetboxStorageOpts
|
|
32
|
+
|
|
33
|
+
def query(self):
|
|
34
|
+
return NetboxQuery
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
from annet.adapters.netbox.common.models import Entity, DeviceType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Label:
|
|
10
|
+
value: int
|
|
11
|
+
label: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DeviceIp:
|
|
16
|
+
id: int
|
|
17
|
+
address: str
|
|
18
|
+
family: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Device(Entity):
|
|
23
|
+
url: str
|
|
24
|
+
display_name: str
|
|
25
|
+
device_type: DeviceType
|
|
26
|
+
device_role: Entity
|
|
27
|
+
tenant: Optional[Entity]
|
|
28
|
+
platform: Optional[Entity]
|
|
29
|
+
serial: str
|
|
30
|
+
asset_tag: Optional[str]
|
|
31
|
+
site: Entity
|
|
32
|
+
rack: Optional[Entity]
|
|
33
|
+
position: Optional[float]
|
|
34
|
+
face: Optional[Label]
|
|
35
|
+
status: Label
|
|
36
|
+
primary_ip: Optional[DeviceIp]
|
|
37
|
+
primary_ip4: Optional[DeviceIp]
|
|
38
|
+
primary_ip6: Optional[DeviceIp]
|
|
39
|
+
tags: List[str]
|
|
40
|
+
custom_fields: Dict[str, Any]
|
|
41
|
+
created: datetime
|
|
42
|
+
last_updated: datetime
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Interface(Entity):
|
|
47
|
+
device: Entity
|
|
48
|
+
enabled: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Vrf(Entity):
|
|
53
|
+
rd: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class IpAddress:
|
|
58
|
+
id: int
|
|
59
|
+
family: int
|
|
60
|
+
address: str
|
|
61
|
+
vrf: Optional[Vrf]
|
|
62
|
+
tenant: Any # ???
|
|
63
|
+
status: Label
|
|
64
|
+
description: Optional[str]
|
|
65
|
+
custom_fields: Dict[str, Any]
|
|
66
|
+
tags: List[str]
|
|
67
|
+
created: datetime
|
|
68
|
+
last_updated: datetime
|
|
69
|
+
|
|
70
|
+
interface: Entity
|
|
71
|
+
|
|
72
|
+
nat_inside: Any # ???
|
|
73
|
+
nat_outside: Any # ???
|