annet 0.16.8__tar.gz → 0.16.9__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.16.8/annet.egg-info → annet-0.16.9}/PKG-INFO +3 -1
- {annet-0.16.8 → annet-0.16.9}/README.md +70 -10
- annet-0.16.9/annet/adapters/fetchers/stub/fetcher.py +19 -0
- annet-0.16.9/annet/adapters/file/provider.py +226 -0
- annet-0.16.9/annet/adapters/netbox/common/models.py +276 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/v37/storage.py +31 -3
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/netdev/views/hardware.py +31 -0
- annet-0.16.9/annet/bgp_models.py +266 -0
- {annet-0.16.8 → annet-0.16.9}/annet/configs/context.yml +2 -3
- {annet-0.16.8 → annet-0.16.9}/annet/configs/logging.yaml +1 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/__init__.py +18 -4
- annet-0.16.9/annet/mesh/__init__.py +16 -0
- annet-0.16.9/annet/mesh/basemodel.py +180 -0
- annet-0.16.9/annet/mesh/device_models.py +62 -0
- annet-0.16.9/annet/mesh/executor.py +248 -0
- annet-0.16.9/annet/mesh/match_args.py +165 -0
- annet-0.16.9/annet/mesh/models_converter.py +84 -0
- annet-0.16.9/annet/mesh/peer_models.py +98 -0
- annet-0.16.9/annet/mesh/registry.py +212 -0
- annet-0.16.9/annet/rulebook/nexus/__init__.py +0 -0
- annet-0.16.9/annet/rulebook/routeros/__init__.py +0 -0
- annet-0.16.9/annet/rulebook/routeros/file.py +5 -0
- annet-0.16.9/annet/rulebook/texts/pc.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/storage.py +41 -2
- {annet-0.16.8 → annet-0.16.9/annet.egg-info}/PKG-INFO +3 -1
- {annet-0.16.8 → annet-0.16.9}/annet.egg-info/SOURCES.txt +20 -1
- {annet-0.16.8 → annet-0.16.9}/annet.egg-info/requires.txt +4 -0
- annet-0.16.9/annet_generators/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet_generators/example/__init__.py +1 -3
- annet-0.16.9/annet_generators/mesh_example/__init__.py +9 -0
- annet-0.16.9/annet_generators/mesh_example/bgp.py +43 -0
- annet-0.16.9/annet_generators/mesh_example/mesh_logic.py +28 -0
- {annet-0.16.8 → annet-0.16.9}/requirements.txt +2 -0
- annet-0.16.8/annet/adapters/netbox/common/models.py +0 -171
- {annet-0.16.8 → annet-0.16.9}/AUTHORS +0 -0
- {annet-0.16.8 → annet-0.16.9}/LICENSE +0 -0
- {annet-0.16.8 → annet-0.16.9}/MANIFEST.in +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/__init__.py +0 -0
- {annet-0.16.8/annet/adapters/netbox → annet-0.16.9/annet/adapters/fetchers}/__init__.py +0 -0
- {annet-0.16.8/annet/adapters/netbox/common → annet-0.16.9/annet/adapters/fetchers/stub}/__init__.py +0 -0
- {annet-0.16.8/annet/adapters/netbox/v24 → annet-0.16.9/annet/adapters/file}/__init__.py +0 -0
- {annet-0.16.8/annet/adapters/netbox/v37 → annet-0.16.9/annet/adapters/netbox}/__init__.py +0 -0
- {annet-0.16.8/annet/annlib/netdev → annet-0.16.9/annet/adapters/netbox/common}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/common/client.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/common/manufacturer.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/common/query.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/common/status_client.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/common/storage_opts.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/provider.py +0 -0
- {annet-0.16.8/annet/annlib/netdev/views → annet-0.16.9/annet/adapters/netbox/v24}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/adapters/netbox/v24/storage.py +0 -0
- {annet-0.16.8/annet/annlib/rbparser → annet-0.16.9/annet/adapters/netbox/v37}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annet.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/command.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/diff.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/errors.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/filter_acl.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/jsontools.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/lib.py +0 -0
- {annet-0.16.8/annet/annlib/rulebook → annet-0.16.9/annet/annlib/netdev}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/netdev/db.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/netdev/devdb/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/netdev/devdb/data/devdb.json +0 -0
- {annet-0.16.8/annet/generators/common → annet-0.16.9/annet/annlib/netdev/views}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/netdev/views/dump.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/output.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/patching.py +0 -0
- {annet-0.16.8/annet/rulebook/arista → annet-0.16.9/annet/annlib/rbparser}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/rbparser/acl.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/rbparser/deploying.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/rbparser/ordering.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/rbparser/platform.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/rbparser/syntax.py +0 -0
- {annet-0.16.8/annet/rulebook/b4com → annet-0.16.9/annet/annlib/rulebook}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/rulebook/common.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/tabparser.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/annlib/types.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/api/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/argparse.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/cli.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/cli_args.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/connectors.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/deploy.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/diff.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/executor.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/filtering.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/gen.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/base.py +0 -0
- {annet-0.16.8/annet/rulebook/cisco → annet-0.16.9/annet/generators/common}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/common/initial.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/entire.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/exceptions.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/jsonfragment.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/partial.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/perf.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/ref.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/generators/result.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/hardware.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/implicit.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/lib.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/output.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/parallel.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/patching.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/reference.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/__init__.py +0 -0
- {annet-0.16.8/annet/rulebook/huawei → annet-0.16.9/annet/rulebook/arista}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/arista/iface.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/aruba/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/aruba/ap_env.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/aruba/misc.py +0 -0
- {annet-0.16.8/annet/rulebook/nexus → annet-0.16.9/annet/rulebook/b4com}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/b4com/file.py +0 -0
- {annet-0.16.8/annet_generators → annet-0.16.9/annet/rulebook/cisco}/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/cisco/iface.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/cisco/misc.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/cisco/vlandb.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/common.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/deploying.py +0 -0
- /annet-0.16.8/annet/rulebook/texts/pc.deploy → /annet-0.16.9/annet/rulebook/huawei/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/huawei/aaa.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/huawei/bgp.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/huawei/iface.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/huawei/misc.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/huawei/vlandb.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/juniper/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/nexus/iface.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/patching.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/ribbon/__init__.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/arista.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/arista.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/arista.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/aruba.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/aruba.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/aruba.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/b4com.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/b4com.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/b4com.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/cisco.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/cisco.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/cisco.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/huawei.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/huawei.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/huawei.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/juniper.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/nexus.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/nexus.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/nexus.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/nokia.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/optixtrans.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/optixtrans.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/optixtrans.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/pc.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/pc.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/ribbon.deploy +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/ribbon.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/routeros.order +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/rulebook/texts/routeros.rul +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/tabparser.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/text_term_format.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/tracing.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet/types.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet.egg-info/dependency_links.txt +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet.egg-info/entry_points.txt +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet.egg-info/top_level.txt +0 -0
- {annet-0.16.8 → annet-0.16.9}/annet_generators/example/lldp.py +0 -0
- {annet-0.16.8 → annet-0.16.9}/setup.cfg +0 -0
- {annet-0.16.8 → annet-0.16.9}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: annet
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.9
|
|
4
4
|
Summary: annet
|
|
5
5
|
Home-page: https://github.com/annetutil/annet
|
|
6
6
|
License: MIT
|
|
@@ -23,3 +23,5 @@ Requires-Dist: aiohttp>=3.8.4
|
|
|
23
23
|
Requires-Dist: yarl>=1.8.2
|
|
24
24
|
Requires-Dist: adaptix==3.0.0b7
|
|
25
25
|
Requires-Dist: dataclass-rest==0.4
|
|
26
|
+
Requires-Dist: requests>=2.32.3
|
|
27
|
+
Requires-Dist: annetbox>=0.1.8; python_version > "3.9"
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
# Annet -
|
|
1
|
+
# Annet - configuration generation and deploying utility for network equipment
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
3
|
+
Annet is a configuration generator that can translate differences between old and new configurations into sequnce of commands. This feature is vital for CLI-based devices, such as Huawei, Cisco IOS, Cisco NX-OS, Juniper. Devices configured via separate config files, Linux, FreeBSD and Cumulus are also supported.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
It works this way. Annet `gen`erates configuration for a device by running Python code, which usually goes to the Network Source of Truth, like NetBox. Annet then gets the `diff`erence by getting the configuration from the device and comparing it. Finally, Annet translates the difference into a sequence of commands, called a `patch`. After `deploy`ing these commands, the diff will be empty.
|
|
6
|
+
|
|
7
|
+
Annet has a number of modes (subcommands):
|
|
7
8
|
|
|
8
9
|
- ```annet gen``` - generates the entire config for the specified devices or specified parts of it
|
|
9
10
|
- ```annet diff``` - first does gen and then builds diff with current config version
|
|
10
11
|
- ```annet patch``` - first does diff and then generates a list of commands to apply diff on the device
|
|
12
|
+
- ```annet deploy``` - first does patch and then deploys it to the device
|
|
11
13
|
|
|
12
14
|
Usage help can be obtained by calling ```annet -h``` or for a specific command, such as ```annet gen -h```.
|
|
13
15
|
|
|
@@ -68,7 +70,7 @@ So you need to write the undo logic for the ```rule ```` command in the ``acl ``
|
|
|
68
70
|
Here is the part of rulebook/texts/huawei.rul responsible for this:
|
|
69
71
|
```
|
|
70
72
|
acl name *
|
|
71
|
-
|
|
73
|
+
rule * %logic=huawei.misc.undo_redo
|
|
72
74
|
```
|
|
73
75
|
The asterisk here means that the key argument of the undo_redo function will contain the first word after rule, namely the rule number.
|
|
74
76
|
|
|
@@ -77,7 +79,7 @@ Here, the undo_redo function from the file in rulebook/huawei/misc.py is used to
|
|
|
77
79
|
def undo_redo(rule, key, diff, **_):
|
|
78
80
|
...
|
|
79
81
|
```
|
|
80
|
-
Now calling
|
|
82
|
+
Now calling `annet patch -g snmp sw1-i38` returns the correct set of commands.
|
|
81
83
|
```
|
|
82
84
|
acl number 2610
|
|
83
85
|
undo rule 40
|
|
@@ -85,21 +87,62 @@ acl number 2610
|
|
|
85
87
|
quit
|
|
86
88
|
```
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
<!-- ### annet deploy
|
|
90
|
+
### annet deploy
|
|
90
91
|
|
|
91
92
|
To apply these commands on a switch there is a **deploy** module.
|
|
92
|
-
|
|
93
|
+
Annet can apply changes (roll out) to multiple devices at the same time.
|
|
93
94
|
|
|
94
95
|
By default, the edits that annet proposes to roll out will be shown before the rollout.
|
|
95
96
|
The user must confirm that they agree to roll out the proposed diff to a given list of devices.
|
|
96
97
|
During the rollout, annet will display the overall progress of the task and the log of one of the devices.
|
|
97
98
|
|
|
98
99
|
Normal layout. The screen with patches will be shown and the process of laying out will be displayed.
|
|
100
|
+
|
|
99
101
|
```bash
|
|
100
102
|
annet deploy -g snmp $HOST
|
|
101
103
|
```
|
|
102
|
-
|
|
104
|
+
|
|
105
|
+
Credentials will be used from the current user (username, ssh key, ssh agent).
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
```shell
|
|
110
|
+
mkdir myproject
|
|
111
|
+
cd myproject
|
|
112
|
+
python3 -m venv venv
|
|
113
|
+
source venv/bin/activate
|
|
114
|
+
pip install annet gnetcli_adapter
|
|
115
|
+
|
|
116
|
+
cat > ~/.annet/context.yml_tmp<<EOF
|
|
117
|
+
fetcher:
|
|
118
|
+
default:
|
|
119
|
+
adapter: gnetcli
|
|
120
|
+
deployer:
|
|
121
|
+
default:
|
|
122
|
+
adapter: gnetcli
|
|
123
|
+
generators:
|
|
124
|
+
default:
|
|
125
|
+
- my_generators
|
|
126
|
+
storage:
|
|
127
|
+
netbox:
|
|
128
|
+
adapter: netbox
|
|
129
|
+
params:
|
|
130
|
+
url: http://127.0.0.1:8000
|
|
131
|
+
token: 1234567890abcdef01234567890abcdef0123456
|
|
132
|
+
context:
|
|
133
|
+
default:
|
|
134
|
+
fetcher: default
|
|
135
|
+
deployer: default
|
|
136
|
+
connection: default
|
|
137
|
+
generators: default
|
|
138
|
+
storage: default
|
|
139
|
+
selected_context: default
|
|
140
|
+
EOF
|
|
141
|
+
|
|
142
|
+
cp -r my_generators
|
|
143
|
+
|
|
144
|
+
annet deploy mydevice
|
|
145
|
+
```
|
|
103
146
|
|
|
104
147
|
## Configuration
|
|
105
148
|
|
|
@@ -174,3 +217,20 @@ devices:
|
|
|
174
217
|
|
|
175
218
|
Annet uses [Entry Points](https://setuptools.pypa.io/en/latest/userguide/entry_point.html) mechanism for customization.
|
|
176
219
|
For example, you can implement the Storage interface on top of your favorite inventory system.
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
## Building doc
|
|
223
|
+
|
|
224
|
+
1. Install dependencies:
|
|
225
|
+
|
|
226
|
+
```shell
|
|
227
|
+
pip install -r requirements-doc.txt
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
2. Build
|
|
231
|
+
|
|
232
|
+
```shell
|
|
233
|
+
sphinx-build -M html docs docs-build
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
3. Open rendered html in browser [docs-build/html/index.html](docs-build/html/index.html)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from annet.deploy import Fetcher
|
|
2
|
+
from annet.connectors import AdapterWithConfig
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
from annet.storage import Device
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StubFetcher(Fetcher, AdapterWithConfig):
|
|
8
|
+
@classmethod
|
|
9
|
+
def with_config(cls, **kwargs: Dict[str, Any]) -> Fetcher:
|
|
10
|
+
return cls(**kwargs)
|
|
11
|
+
|
|
12
|
+
def fetch_packages(self, devices: List[Device],
|
|
13
|
+
processes: int = 1, max_slots: int = 0):
|
|
14
|
+
raise NotImplementedError()
|
|
15
|
+
|
|
16
|
+
def fetch(self, devices: List[Device],
|
|
17
|
+
files_to_download: Dict[str, List[str]] = None,
|
|
18
|
+
processes: int = 1, max_slots: int = 0):
|
|
19
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from annet.annlib.netdev.views.dump import DumpableView
|
|
2
|
+
from annet.storage import Query
|
|
3
|
+
from dataclasses import dataclass, fields
|
|
4
|
+
from typing import List, Iterable, Optional, Any
|
|
5
|
+
from annet.storage import StorageProvider, Storage
|
|
6
|
+
from annet.connectors import AdapterWithName
|
|
7
|
+
from annet.storage import Device as DeviceCls
|
|
8
|
+
from annet.annlib.netdev.views.hardware import vendor_to_hw, HardwareView
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Interface(DumpableView):
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
enabled: bool = True
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def _dump__list_key(self):
|
|
20
|
+
return self.name
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DeviceStorage:
|
|
25
|
+
fqdn: str
|
|
26
|
+
vendor: str
|
|
27
|
+
hostname: Optional[str] = None
|
|
28
|
+
serial: Optional[str] = None
|
|
29
|
+
id: Optional[str] = None
|
|
30
|
+
interfaces: Optional[list[Interface]] = None
|
|
31
|
+
storage: Optional[Storage] = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
if not self.id:
|
|
35
|
+
self.id = self.fqdn
|
|
36
|
+
if not self.hostname:
|
|
37
|
+
self.hostname = self.fqdn.split(".")[0]
|
|
38
|
+
hw = vendor_to_hw(self.vendor)
|
|
39
|
+
if not hw:
|
|
40
|
+
raise Exception("unknown vendor")
|
|
41
|
+
self.hw = hw
|
|
42
|
+
if isinstance(self.interfaces, list):
|
|
43
|
+
interfaces = []
|
|
44
|
+
for iface in self.interfaces:
|
|
45
|
+
try:
|
|
46
|
+
interfaces.append(Interface(**iface))
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise Exception("unable to parse %s as Interface %s" % (iface, e))
|
|
49
|
+
self.interfaces = interfaces
|
|
50
|
+
|
|
51
|
+
def set_storage(self, storage: Storage):
|
|
52
|
+
self.storage = storage
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Device(DeviceCls, DumpableView):
|
|
57
|
+
dev: DeviceStorage
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def hostname(self) -> str:
|
|
61
|
+
return self.dev.hostname
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def fqdn(self) -> str:
|
|
65
|
+
return self.dev.fqdn
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def id(self):
|
|
69
|
+
return self.dev.id
|
|
70
|
+
|
|
71
|
+
def __hash__(self):
|
|
72
|
+
return hash((self.id, type(self)))
|
|
73
|
+
|
|
74
|
+
def __eq__(self, other):
|
|
75
|
+
return type(self) is type(other) and self.fqdn == other.fqdn and self.vendor == other.vendor
|
|
76
|
+
|
|
77
|
+
def is_pc(self) -> bool:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def storage(self) -> Storage:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def hw(self) -> HardwareView:
|
|
86
|
+
return self.dev.hw
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def breed(self) -> str:
|
|
90
|
+
return self.dev.hw.vendor
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def neighbours_ids(self):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class Devices:
|
|
99
|
+
devices: list[Device]
|
|
100
|
+
|
|
101
|
+
def __post_init__(self):
|
|
102
|
+
if isinstance(self.devices, list):
|
|
103
|
+
devices = []
|
|
104
|
+
for dev in self.devices:
|
|
105
|
+
try:
|
|
106
|
+
devices.append(Device(dev=DeviceStorage(**dev)))
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise Exception("unable to parse %s as Device %s" % (dev, e))
|
|
109
|
+
self.devices = devices
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Provider(StorageProvider, AdapterWithName):
|
|
113
|
+
def storage(self):
|
|
114
|
+
return storage_factory
|
|
115
|
+
|
|
116
|
+
def opts(self):
|
|
117
|
+
return StorageOpts
|
|
118
|
+
|
|
119
|
+
def query(self):
|
|
120
|
+
return Query
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def name(cls) -> str:
|
|
124
|
+
return "file"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class Query(Query):
|
|
129
|
+
query: List[str]
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def new(cls, query: str | Iterable[str], hosts_range: Optional[slice] = None) -> "Query":
|
|
133
|
+
if hosts_range is not None:
|
|
134
|
+
raise ValueError("host_range is not supported")
|
|
135
|
+
return cls(query=list(query))
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def globs(self):
|
|
139
|
+
return self.query
|
|
140
|
+
|
|
141
|
+
def is_empty(self) -> bool:
|
|
142
|
+
return len(self.query) == 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class StorageOpts:
|
|
146
|
+
def __init__(self, path: str):
|
|
147
|
+
self.path = path
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def parse_params(cls, conf_params: Optional[dict[str, str]], cli_opts: Any):
|
|
151
|
+
path = conf_params.get("path")
|
|
152
|
+
if not path:
|
|
153
|
+
raise Exception("empty path")
|
|
154
|
+
return cls(path=path)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def storage_factory(opts: StorageOpts) -> Storage:
|
|
158
|
+
return FS(opts)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class FS(Storage):
|
|
162
|
+
def __init__(self, opts: StorageOpts):
|
|
163
|
+
self.opts = opts
|
|
164
|
+
self.inventory: Devices = read_inventory(opts.path, self)
|
|
165
|
+
|
|
166
|
+
def __enter__(self):
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
def __exit__(self, _, __, ___):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def resolve_object_ids_by_query(self, query: Query) -> list[str]:
|
|
173
|
+
result = filter_query(self.inventory.devices, query)
|
|
174
|
+
return [dev.fqdn for dev in result]
|
|
175
|
+
|
|
176
|
+
def resolve_fdnds_by_query(self, query: Query) -> list[str]:
|
|
177
|
+
result = filter_query(self.inventory.devices, query)
|
|
178
|
+
return [dev.fqdn for dev in result]
|
|
179
|
+
|
|
180
|
+
def make_devices(
|
|
181
|
+
self,
|
|
182
|
+
query: Query | list,
|
|
183
|
+
preload_neighbors=False,
|
|
184
|
+
use_mesh=None,
|
|
185
|
+
preload_extra_fields=False,
|
|
186
|
+
**kwargs,
|
|
187
|
+
) -> list[Device]:
|
|
188
|
+
if isinstance(query, list):
|
|
189
|
+
query = Query.new(query)
|
|
190
|
+
result = filter_query(self.inventory.devices, query)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
def get_device(self, obj_id: str, preload_neighbors=False, use_mesh=None, **kwargs) -> Device:
|
|
194
|
+
result = filter_query(self.inventory.devices, Query.new(obj_id))
|
|
195
|
+
if not result:
|
|
196
|
+
raise Exception("not found")
|
|
197
|
+
return result[0]
|
|
198
|
+
|
|
199
|
+
def flush_perf(self):
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def filter_query(devices: list[Device], query: Query) -> list[Device]:
|
|
204
|
+
result: list[Device] = []
|
|
205
|
+
for dev in devices:
|
|
206
|
+
if dev.fqdn in query.query:
|
|
207
|
+
result.append(dev)
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def read_inventory(path: str, storage: Storage) -> Devices:
|
|
212
|
+
with open(path, "r") as f:
|
|
213
|
+
data = f.read()
|
|
214
|
+
file_data = yaml.load(data, Loader=yaml.BaseLoader)
|
|
215
|
+
res = dataclass_from_dict(Devices, file_data)
|
|
216
|
+
for dev in res.devices:
|
|
217
|
+
dev.dev.set_storage(storage)
|
|
218
|
+
return res
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def dataclass_from_dict(klass: type, d: dict[str, Any]):
|
|
222
|
+
try:
|
|
223
|
+
fieldtypes = {f.name: f.type for f in fields(klass)}
|
|
224
|
+
except TypeError:
|
|
225
|
+
return d
|
|
226
|
+
return klass(**{f: dataclass_from_dict(fieldtypes[f], d[f]) for f in d})
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from ipaddress import ip_interface, IPv6Interface
|
|
4
|
+
from typing import List, Optional, Any, Dict, Sequence, Callable
|
|
5
|
+
|
|
6
|
+
from annet.annlib.netdev.views.dump import DumpableView
|
|
7
|
+
from annet.annlib.netdev.views.hardware import HardwareView, lag_name, svi_name
|
|
8
|
+
from annet.storage import Storage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Entity(DumpableView):
|
|
13
|
+
id: int
|
|
14
|
+
name: str
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def _dump__list_key(self):
|
|
18
|
+
return self.name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Label:
|
|
23
|
+
value: str
|
|
24
|
+
label: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class IpFamily:
|
|
29
|
+
value: int
|
|
30
|
+
label: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DeviceType:
|
|
35
|
+
id: int
|
|
36
|
+
manufacturer: Entity
|
|
37
|
+
model: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DeviceIp(DumpableView):
|
|
42
|
+
id: int
|
|
43
|
+
display: str
|
|
44
|
+
address: str
|
|
45
|
+
family: int
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def _dump__list_key(self):
|
|
49
|
+
return self.address
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Prefix(DumpableView):
|
|
54
|
+
id: int
|
|
55
|
+
prefix: str
|
|
56
|
+
site: Optional[Entity]
|
|
57
|
+
vrf: Optional[Entity]
|
|
58
|
+
tenant: Optional[Entity]
|
|
59
|
+
vlan: Optional[Entity]
|
|
60
|
+
role: Optional[Entity]
|
|
61
|
+
status: Label
|
|
62
|
+
is_pool: bool
|
|
63
|
+
custom_fields: dict[str, Any]
|
|
64
|
+
created: datetime
|
|
65
|
+
last_updated: datetime
|
|
66
|
+
description: Optional[str] = ""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def _dump__list_key(self):
|
|
70
|
+
return self.prefix
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class IpAddress(DumpableView):
|
|
75
|
+
id: int
|
|
76
|
+
assigned_object_id: int
|
|
77
|
+
display: str
|
|
78
|
+
family: IpFamily
|
|
79
|
+
address: str
|
|
80
|
+
status: Label
|
|
81
|
+
tags: List[Entity]
|
|
82
|
+
created: datetime
|
|
83
|
+
last_updated: datetime
|
|
84
|
+
prefix: Optional[Prefix] = None
|
|
85
|
+
vrf: Optional[Entity] = None
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def _dump__list_key(self):
|
|
89
|
+
return self.address
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class InterfaceConnectedEndpoint(Entity):
|
|
94
|
+
device: Entity
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class InterfaceType:
|
|
99
|
+
value: str
|
|
100
|
+
label: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class InterfaceMode:
|
|
105
|
+
value: str
|
|
106
|
+
label: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class InterfaceVlan(Entity):
|
|
111
|
+
vid: int
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class Interface(Entity):
|
|
116
|
+
device: Entity
|
|
117
|
+
enabled: bool
|
|
118
|
+
description: str
|
|
119
|
+
type: InterfaceType
|
|
120
|
+
connected_endpoints: Optional[list[InterfaceConnectedEndpoint]]
|
|
121
|
+
mode: Optional[InterfaceMode]
|
|
122
|
+
untagged_vlan: Optional[InterfaceVlan]
|
|
123
|
+
tagged_vlans: Optional[List[InterfaceVlan]]
|
|
124
|
+
display: str = ""
|
|
125
|
+
ip_addresses: List[IpAddress] = field(default_factory=list)
|
|
126
|
+
vrf: Optional[Entity] = None
|
|
127
|
+
mtu: int | None = None
|
|
128
|
+
lag: Entity | None = None
|
|
129
|
+
lag_min_links: int | None = None
|
|
130
|
+
|
|
131
|
+
def add_addr(self, address_mask: str, vrf: str | None) -> None:
|
|
132
|
+
addr = ip_interface(address_mask)
|
|
133
|
+
if vrf is None:
|
|
134
|
+
vrf_obj = None
|
|
135
|
+
else:
|
|
136
|
+
vrf_obj = Entity(id=0, name=vrf)
|
|
137
|
+
|
|
138
|
+
if isinstance(addr, IPv6Interface):
|
|
139
|
+
family = IpFamily(value=6, label="IPv6")
|
|
140
|
+
else:
|
|
141
|
+
family = IpFamily(value=4, label="IPv4")
|
|
142
|
+
self.ip_addresses.append(IpAddress(
|
|
143
|
+
id=0,
|
|
144
|
+
display=address_mask,
|
|
145
|
+
address=address_mask,
|
|
146
|
+
vrf=vrf_obj,
|
|
147
|
+
prefix=None,
|
|
148
|
+
family=family,
|
|
149
|
+
created=datetime.now(timezone.utc),
|
|
150
|
+
last_updated=datetime.now(timezone.utc),
|
|
151
|
+
tags=[],
|
|
152
|
+
status=Label(value="active", label="Active"),
|
|
153
|
+
assigned_object_id=self.id,
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class NetboxDevice(Entity):
|
|
159
|
+
url: str
|
|
160
|
+
storage: Storage
|
|
161
|
+
|
|
162
|
+
display: str
|
|
163
|
+
device_type: DeviceType
|
|
164
|
+
device_role: Entity
|
|
165
|
+
tenant: Optional[Entity]
|
|
166
|
+
platform: Optional[Entity]
|
|
167
|
+
serial: str
|
|
168
|
+
asset_tag: Optional[str]
|
|
169
|
+
site: Entity
|
|
170
|
+
rack: Optional[Entity]
|
|
171
|
+
position: Optional[float]
|
|
172
|
+
face: Optional[Label]
|
|
173
|
+
status: Label
|
|
174
|
+
primary_ip: Optional[DeviceIp]
|
|
175
|
+
primary_ip4: Optional[DeviceIp]
|
|
176
|
+
primary_ip6: Optional[DeviceIp]
|
|
177
|
+
tags: List[Entity]
|
|
178
|
+
custom_fields: Dict[str, Any]
|
|
179
|
+
created: datetime
|
|
180
|
+
last_updated: datetime
|
|
181
|
+
|
|
182
|
+
fqdn: str
|
|
183
|
+
hostname: str
|
|
184
|
+
hw: Optional[HardwareView]
|
|
185
|
+
breed: str
|
|
186
|
+
|
|
187
|
+
interfaces: List[Interface]
|
|
188
|
+
neighbours: Optional[List["NetboxDevice"]]
|
|
189
|
+
|
|
190
|
+
# compat
|
|
191
|
+
@property
|
|
192
|
+
def neighbours_fqdns(self) -> list[str]:
|
|
193
|
+
if not self.neighbours:
|
|
194
|
+
return []
|
|
195
|
+
return [dev.fqdn for dev in self.neighbours]
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def neighbours_ids(self):
|
|
199
|
+
if not self.neighbours:
|
|
200
|
+
return []
|
|
201
|
+
return [dev.id for dev in self.neighbours]
|
|
202
|
+
|
|
203
|
+
def __hash__(self):
|
|
204
|
+
return hash((self.id, type(self)))
|
|
205
|
+
|
|
206
|
+
def __eq__(self, other):
|
|
207
|
+
return type(self) is type(other) and self.url == other.url
|
|
208
|
+
|
|
209
|
+
def is_pc(self) -> bool:
|
|
210
|
+
return self.device_type.manufacturer.name == "Mellanox"
|
|
211
|
+
|
|
212
|
+
def _make_interface(self, name: str, type: InterfaceType) -> Interface:
|
|
213
|
+
return Interface(
|
|
214
|
+
name=name,
|
|
215
|
+
device=self,
|
|
216
|
+
enabled=True,
|
|
217
|
+
description="",
|
|
218
|
+
type=type,
|
|
219
|
+
id=0,
|
|
220
|
+
vrf=None,
|
|
221
|
+
display=name,
|
|
222
|
+
untagged_vlan=None,
|
|
223
|
+
tagged_vlans=[],
|
|
224
|
+
ip_addresses=[],
|
|
225
|
+
connected_endpoints=[],
|
|
226
|
+
mode=None,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _lag_name(self, lag: int) -> str:
|
|
230
|
+
return lag_name(self.hw, lag)
|
|
231
|
+
|
|
232
|
+
def make_lag(self, lag: int, ports: Sequence[str], lag_min_links: int | None) -> Interface:
|
|
233
|
+
new_name = self._lag_name(lag)
|
|
234
|
+
for target_interface in self.interfaces:
|
|
235
|
+
if target_interface.name == new_name:
|
|
236
|
+
return target_interface
|
|
237
|
+
lag_interface = self._make_interface(
|
|
238
|
+
name=new_name,
|
|
239
|
+
type=InterfaceType(value="lag", label="Link Aggregation Group (LAG)"),
|
|
240
|
+
)
|
|
241
|
+
lag_interface.lag_min_links = lag_min_links
|
|
242
|
+
for interface in self.interfaces:
|
|
243
|
+
if interface.name in ports:
|
|
244
|
+
interface.lag = lag_interface
|
|
245
|
+
self.interfaces.append(lag_interface)
|
|
246
|
+
return lag_interface
|
|
247
|
+
|
|
248
|
+
def _svi_name(self, svi: int) -> str:
|
|
249
|
+
return svi_name(self.hw, svi)
|
|
250
|
+
|
|
251
|
+
def add_svi(self, svi: int) -> Interface:
|
|
252
|
+
name = self._svi_name(svi)
|
|
253
|
+
for interface in self.interfaces:
|
|
254
|
+
if interface.name == name:
|
|
255
|
+
return interface
|
|
256
|
+
interface = self._make_interface(
|
|
257
|
+
name=name,
|
|
258
|
+
type=InterfaceType("virtual", "Virtual")
|
|
259
|
+
)
|
|
260
|
+
self.interfaces.append(interface)
|
|
261
|
+
return interface
|
|
262
|
+
|
|
263
|
+
def _subif_name(self, interface: str, subif: int) -> str:
|
|
264
|
+
return f"{interface}.{subif}"
|
|
265
|
+
|
|
266
|
+
def add_subif(self, interface: str, subif: int) -> Interface:
|
|
267
|
+
name = self._subif_name(interface, subif)
|
|
268
|
+
for target_port in self.interfaces:
|
|
269
|
+
if target_port.name == name:
|
|
270
|
+
return target_port
|
|
271
|
+
target_port = self._make_interface(
|
|
272
|
+
name=name,
|
|
273
|
+
type=InterfaceType("virtual", "Virtual")
|
|
274
|
+
)
|
|
275
|
+
self.interfaces.append(target_port)
|
|
276
|
+
return target_port
|