annet 1.0.2__tar.gz → 1.0.4__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-1.0.2/annet.egg-info → annet-1.0.4}/PKG-INFO +2 -2
- {annet-1.0.2 → annet-1.0.4}/README.md +5 -0
- {annet-1.0.2 → annet-1.0.4}/annet/__init__.py +0 -5
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/models.py +12 -7
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/storage_opts.py +17 -2
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/v37/storage.py +5 -36
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/devdb/data/devdb.json +3 -0
- {annet-1.0.2 → annet-1.0.4}/annet/bgp_models.py +84 -1
- {annet-1.0.2 → annet-1.0.4}/annet/connectors.py +12 -15
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/device_models.py +12 -0
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/executor.py +9 -8
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/models_converter.py +3 -1
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/peer_models.py +1 -0
- annet-1.0.4/annet/rulebook/arista/aaa.py +63 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/arista.rul +1 -1
- {annet-1.0.2 → annet-1.0.4/annet.egg-info}/PKG-INFO +2 -2
- {annet-1.0.2 → annet-1.0.4}/annet.egg-info/SOURCES.txt +1 -0
- {annet-1.0.2 → annet-1.0.4}/annet.egg-info/requires.txt +1 -1
- {annet-1.0.2 → annet-1.0.4}/setup.py +1 -1
- {annet-1.0.2 → annet-1.0.4}/AUTHORS +0 -0
- {annet-1.0.2 → annet-1.0.4}/LICENSE +0 -0
- {annet-1.0.2 → annet-1.0.4}/MANIFEST.in +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/fetchers/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/fetchers/stub/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/fetchers/stub/fetcher.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/file/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/file/provider.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/client.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/manufacturer.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/query.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/common/status_client.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/provider.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/v24/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/v24/storage.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/adapters/netbox/v37/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annet.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/command.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/diff.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/errors.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/filter_acl.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/jsontools.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/lib.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/db.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/devdb/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/views/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/views/dump.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/netdev/views/hardware.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/output.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/patching.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rbparser/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rbparser/acl.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rbparser/deploying.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rbparser/ordering.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rbparser/platform.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rbparser/syntax.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rulebook/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/rulebook/common.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/tabparser.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/annlib/types.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/api/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/argparse.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/cli.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/cli_args.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/configs/context.yml +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/configs/logging.yaml +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/deploy.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/deploy_ui.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/diff.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/executor.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/filtering.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/gen.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/base.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/common/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/common/initial.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/entire.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/exceptions.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/jsonfragment.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/partial.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/perf.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/ref.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/generators/result.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/hardware.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/implicit.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/lib.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/basemodel.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/match_args.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/port_processor.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/mesh/registry.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/output.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/parallel.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/patching.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/reference.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/action.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/condition.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/match_builder.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/policy.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/result.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/routemap.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl/statement_builder.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/aspath.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/community.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/cumulus_frr.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/entities.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/execute.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/policy.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/prefix_lists.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rpl_generators/rd.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/arista/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/arista/iface.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/aruba/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/aruba/ap_env.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/aruba/misc.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/b4com/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/b4com/file.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/b4com/iface.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/cisco/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/cisco/iface.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/cisco/misc.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/cisco/vlandb.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/common.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/deploying.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/huawei/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/huawei/aaa.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/huawei/bgp.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/huawei/iface.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/huawei/misc.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/huawei/vlandb.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/juniper/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/nexus/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/nexus/iface.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/patching.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/ribbon/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/routeros/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/routeros/file.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/arista.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/arista.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/aruba.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/aruba.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/aruba.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/b4com.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/b4com.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/b4com.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/cisco.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/cisco.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/cisco.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/huawei.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/huawei.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/huawei.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/juniper.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/nexus.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/nexus.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/nexus.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/nokia.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/optixtrans.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/optixtrans.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/optixtrans.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/pc.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/pc.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/pc.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/ribbon.deploy +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/ribbon.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/routeros.order +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/rulebook/texts/routeros.rul +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/storage.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/tabparser.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/text_term_format.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/tracing.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet/types.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet.egg-info/dependency_links.txt +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet.egg-info/entry_points.txt +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet.egg-info/top_level.txt +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/example/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/example/lldp.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/mesh_example/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/mesh_example/bgp.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/mesh_example/mesh_logic.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/rpl_example/__init__.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/rpl_example/generator.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/rpl_example/items.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/rpl_example/mesh.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/annet_generators/rpl_example/route_policy.py +0 -0
- {annet-1.0.2 → annet-1.0.4}/requirements.txt +0 -0
- {annet-1.0.2 → annet-1.0.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: annet
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: annet
|
|
5
5
|
Home-page: https://github.com/annetutil/annet
|
|
6
6
|
License: MIT
|
|
@@ -23,7 +23,7 @@ Requires-Dist: yarl>=1.8.2
|
|
|
23
23
|
Requires-Dist: adaptix==3.0.0b7
|
|
24
24
|
Requires-Dist: dataclass-rest==0.4
|
|
25
25
|
Provides-Extra: netbox
|
|
26
|
-
Requires-Dist: annetbox[sync]>=0.
|
|
26
|
+
Requires-Dist: annetbox[sync]>=0.2.0; extra == "netbox"
|
|
27
27
|
Dynamic: home-page
|
|
28
28
|
Dynamic: license
|
|
29
29
|
Dynamic: provides-extra
|
|
@@ -68,3 +68,8 @@ sphinx-build -M html docs docs-build
|
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
3. Open rendered html in browser docs-build/html/index.html
|
|
71
|
+
|
|
72
|
+
## Links
|
|
73
|
+
|
|
74
|
+
* [Online Documentation](https://annetutil.github.io/annet/)
|
|
75
|
+
* [Tutorial](https://annetutil.github.io/annet/main/usage/tutorial.html)
|
|
@@ -49,11 +49,6 @@ def init(options: Namespace):
|
|
|
49
49
|
colorama.init = lambda *_, **__: None
|
|
50
50
|
colorama.init()
|
|
51
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
52
|
|
|
58
53
|
def assert_python_version():
|
|
59
54
|
if sys.version_info < (3, 10, 0):
|
|
@@ -192,20 +192,25 @@ class NetboxDevice(Entity):
|
|
|
192
192
|
breed: str
|
|
193
193
|
|
|
194
194
|
interfaces: List[Interface]
|
|
195
|
-
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def neighbors(self) -> List["Entity"]:
|
|
198
|
+
return [
|
|
199
|
+
endpoint.device
|
|
200
|
+
for iface in self.interfaces
|
|
201
|
+
if iface.connected_endpoints
|
|
202
|
+
for endpoint in iface.connected_endpoints
|
|
203
|
+
if endpoint.device
|
|
204
|
+
]
|
|
196
205
|
|
|
197
206
|
# compat
|
|
198
207
|
@property
|
|
199
208
|
def neighbours_fqdns(self) -> list[str]:
|
|
200
|
-
|
|
201
|
-
return []
|
|
202
|
-
return [dev.fqdn for dev in self.neighbours]
|
|
209
|
+
return [dev.name for dev in self.neighbors]
|
|
203
210
|
|
|
204
211
|
@property
|
|
205
212
|
def neighbours_ids(self):
|
|
206
|
-
|
|
207
|
-
return []
|
|
208
|
-
return [dev.id for dev in self.neighbours]
|
|
213
|
+
return [dev.id for dev in self.neighbors]
|
|
209
214
|
|
|
210
215
|
def __hash__(self):
|
|
211
216
|
return hash((self.id, type(self)))
|
|
@@ -5,16 +5,25 @@ DEFAULT_URL = "http://localhost"
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class NetboxStorageOpts:
|
|
8
|
-
def __init__(
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
url: str,
|
|
11
|
+
token: str,
|
|
12
|
+
insecure: bool = False,
|
|
13
|
+
exact_host_filter: bool = False,
|
|
14
|
+
threads: int = 1,
|
|
15
|
+
):
|
|
9
16
|
self.url = url
|
|
10
17
|
self.token = token
|
|
11
18
|
self.insecure = insecure
|
|
12
19
|
self.exact_host_filter = exact_host_filter
|
|
20
|
+
self.threads = threads
|
|
13
21
|
|
|
14
22
|
@classmethod
|
|
15
23
|
def parse_params(cls, conf_params: Optional[dict[str, str]], cli_opts: Any):
|
|
16
24
|
url = os.getenv("NETBOX_URL") or conf_params.get("url") or DEFAULT_URL
|
|
17
25
|
token = os.getenv("NETBOX_TOKEN", "").strip() or conf_params.get("token") or ""
|
|
26
|
+
threads = os.getenv("NETBOX_CLIENT_THREADS", "").strip() or conf_params.get("threads") or "1"
|
|
18
27
|
insecure = False
|
|
19
28
|
if insecure_env := os.getenv("NETBOX_INSECURE", "").lower():
|
|
20
29
|
insecure = insecure_env in ("true", "1", "t")
|
|
@@ -24,4 +33,10 @@ class NetboxStorageOpts:
|
|
|
24
33
|
exact_host_filter = exact_host_filter_env in ("true", "1", "t")
|
|
25
34
|
else:
|
|
26
35
|
exact_host_filter = bool(conf_params.get("exact_host_filter") or False)
|
|
27
|
-
return cls(
|
|
36
|
+
return cls(
|
|
37
|
+
url=url,
|
|
38
|
+
token=token,
|
|
39
|
+
insecure=insecure,
|
|
40
|
+
exact_host_filter=exact_host_filter,
|
|
41
|
+
threads=int(threads),
|
|
42
|
+
)
|
|
@@ -24,7 +24,6 @@ logger = getLogger(__name__)
|
|
|
24
24
|
@impl_converter(recipe=[
|
|
25
25
|
link(P[api_models.Device].name, P[models.NetboxDevice].hostname),
|
|
26
26
|
link(P[api_models.Device].name, P[models.NetboxDevice].fqdn),
|
|
27
|
-
link_constant(P[models.NetboxDevice].neighbours, value=None),
|
|
28
27
|
])
|
|
29
28
|
def extend_device_base(
|
|
30
29
|
device: api_models.Device,
|
|
@@ -39,7 +38,6 @@ def extend_device_base(
|
|
|
39
38
|
def extend_device(
|
|
40
39
|
device: api_models.Device,
|
|
41
40
|
interfaces: List[models.Interface],
|
|
42
|
-
neighbours: Optional[List[models.NetboxDevice]],
|
|
43
41
|
storage: Storage,
|
|
44
42
|
) -> models.NetboxDevice:
|
|
45
43
|
platform_name: str = ""
|
|
@@ -64,7 +62,6 @@ def extend_device(
|
|
|
64
62
|
hw=hw,
|
|
65
63
|
storage=storage,
|
|
66
64
|
)
|
|
67
|
-
res.neighbours = neighbours
|
|
68
65
|
return res
|
|
69
66
|
|
|
70
67
|
|
|
@@ -91,6 +88,7 @@ class NetboxStorageV37(Storage):
|
|
|
91
88
|
url = ""
|
|
92
89
|
token = ""
|
|
93
90
|
self.exact_host_filter = False
|
|
91
|
+
threads = 1
|
|
94
92
|
if opts:
|
|
95
93
|
if opts.insecure:
|
|
96
94
|
ctx = ssl.create_default_context()
|
|
@@ -98,8 +96,10 @@ class NetboxStorageV37(Storage):
|
|
|
98
96
|
ctx.verify_mode = ssl.CERT_NONE
|
|
99
97
|
url = opts.url
|
|
100
98
|
token = opts.token
|
|
99
|
+
threads = opts.threads
|
|
101
100
|
self.exact_host_filter = opts.exact_host_filter
|
|
102
|
-
|
|
101
|
+
|
|
102
|
+
self.netbox = NetboxV37(url=url, token=token, ssl_context=ctx, threads=threads)
|
|
103
103
|
self._all_fqdns: Optional[list[str]] = None
|
|
104
104
|
self._id_devices: dict[int, models.NetboxDevice] = {}
|
|
105
105
|
self._name_devices: dict[str, models.NetboxDevice] = {}
|
|
@@ -174,7 +174,6 @@ class NetboxStorageV37(Storage):
|
|
|
174
174
|
device.id: extend_device(
|
|
175
175
|
device=device,
|
|
176
176
|
interfaces=[],
|
|
177
|
-
neighbours=[],
|
|
178
177
|
storage=self,
|
|
179
178
|
)
|
|
180
179
|
for device in self._load_devices(query)
|
|
@@ -186,16 +185,8 @@ class NetboxStorageV37(Storage):
|
|
|
186
185
|
self._record_device(device)
|
|
187
186
|
|
|
188
187
|
interfaces = self._load_interfaces(list(device_ids))
|
|
189
|
-
neighbours = {x.id: x for x in self._load_neighbours(interfaces)}
|
|
190
|
-
neighbours_seen: dict[str, set] = defaultdict(set)
|
|
191
|
-
|
|
192
188
|
for interface in interfaces:
|
|
193
189
|
device_ids[interface.device.id].interfaces.append(interface)
|
|
194
|
-
for e in interface.connected_endpoints or []:
|
|
195
|
-
neighbour = neighbours[e.device.id]
|
|
196
|
-
if neighbour.id not in neighbours_seen[interface.device.id]:
|
|
197
|
-
neighbours_seen[interface.device.id].add(neighbour.id)
|
|
198
|
-
device_ids[interface.device.id].neighbours.append(neighbour)
|
|
199
190
|
|
|
200
191
|
return list(device_ids.values())
|
|
201
192
|
|
|
@@ -233,8 +224,7 @@ class NetboxStorageV37(Storage):
|
|
|
233
224
|
extended_ifaces[ip.assigned_object_id].ip_addresses.append(ip)
|
|
234
225
|
return list(extended_ifaces.values())
|
|
235
226
|
|
|
236
|
-
def _load_interfaces(self, device_ids: List[int]) -> List[
|
|
237
|
-
models.Interface]:
|
|
227
|
+
def _load_interfaces(self, device_ids: List[int]) -> List[models.Interface]:
|
|
238
228
|
interfaces = self.netbox.dcim_all_interfaces(device_id=device_ids)
|
|
239
229
|
return self._extend_interfaces(interfaces.results)
|
|
240
230
|
|
|
@@ -242,25 +232,6 @@ class NetboxStorageV37(Storage):
|
|
|
242
232
|
interfaces = self.netbox.dcim_all_interfaces_by_id(id=ids)
|
|
243
233
|
return self._extend_interfaces(interfaces.results)
|
|
244
234
|
|
|
245
|
-
def _load_neighbours(self, interfaces: List[models.Interface]) -> List[models.NetboxDevice]:
|
|
246
|
-
endpoints = [e for i in interfaces for e in i.connected_endpoints or []]
|
|
247
|
-
remote_interfaces_ids = [e.id for e in endpoints]
|
|
248
|
-
neighbours_ids = [e.device.id for e in endpoints]
|
|
249
|
-
neighbours_ifaces_dics = defaultdict(list)
|
|
250
|
-
# load only the connected interface to speed things up
|
|
251
|
-
for iface in self._load_interfaces_by_id(remote_interfaces_ids):
|
|
252
|
-
neighbours_ifaces_dics[iface.device.id].append(iface)
|
|
253
|
-
neighbours = []
|
|
254
|
-
for neighbour in self.netbox.dcim_all_devices_by_id(id=neighbours_ids).results:
|
|
255
|
-
extended_neighbour = extend_device(
|
|
256
|
-
device=neighbour,
|
|
257
|
-
storage=self,
|
|
258
|
-
interfaces=neighbours_ifaces_dics[neighbour.id],
|
|
259
|
-
neighbours=None, # do not load recursively
|
|
260
|
-
)
|
|
261
|
-
neighbours.append(extended_neighbour)
|
|
262
|
-
return neighbours
|
|
263
|
-
|
|
264
235
|
def get_device(
|
|
265
236
|
self, obj_id, preload_neighbors=False, use_mesh=None,
|
|
266
237
|
**kwargs,
|
|
@@ -270,13 +241,11 @@ class NetboxStorageV37(Storage):
|
|
|
270
241
|
|
|
271
242
|
device = self.netbox.dcim_device(obj_id)
|
|
272
243
|
interfaces = self._load_interfaces([device.id])
|
|
273
|
-
neighbours = self._load_neighbours(interfaces)
|
|
274
244
|
|
|
275
245
|
res = extend_device(
|
|
276
246
|
device=device,
|
|
277
247
|
storage=self,
|
|
278
248
|
interfaces=interfaces,
|
|
279
|
-
neighbours=neighbours,
|
|
280
249
|
)
|
|
281
250
|
self._record_device(res)
|
|
282
251
|
return res
|
|
@@ -132,6 +132,9 @@
|
|
|
132
132
|
"PC.Whitebox.Ufispace.S": " S",
|
|
133
133
|
"PC.Whitebox.Ufispace.S.S9100": " S91\\d\\d",
|
|
134
134
|
"PC.Whitebox.Ufispace.S.S9100.S9110_32X": " S9110-32X",
|
|
135
|
+
"PC.Whitebox.Ufispace.S.S9300": " S93\\d\\d",
|
|
136
|
+
"PC.Whitebox.Ufispace.S.S9300.S9301_32DB": " S9301-32DB",
|
|
137
|
+
"PC.Whitebox.Ufispace.S.S9300.S9321_64EO": " S9321-64EO",
|
|
135
138
|
"PC.Nebius": "^Nebius",
|
|
136
139
|
"PC.Nebius.NB-E-BR-DCU-AST2600": "^Nebius NB-E-BR-DCU-AST2600",
|
|
137
140
|
|
|
@@ -3,6 +3,77 @@ from dataclasses import dataclass, field
|
|
|
3
3
|
from typing import Literal, Union, Optional
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
class VidRange:
|
|
7
|
+
def __init__(self, start: int, stop: int) -> None:
|
|
8
|
+
self.start = start
|
|
9
|
+
self.stop = stop
|
|
10
|
+
|
|
11
|
+
def is_single(self):
|
|
12
|
+
return self.start == self.stop
|
|
13
|
+
|
|
14
|
+
def __iter__(self):
|
|
15
|
+
return iter(range(self.start, self.stop + 1))
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
if self.is_single():
|
|
19
|
+
return str(self.start)
|
|
20
|
+
return f"{self.start}-{self.stop}"
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
return f"VlanRange({self.start}, {self.stop})"
|
|
24
|
+
|
|
25
|
+
def __eq__(self, other: object) -> bool:
|
|
26
|
+
if type(other) is VidRange:
|
|
27
|
+
return self.start == other.start and self.stop == other.stop
|
|
28
|
+
return NotImplemented
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_vlan_ranges(ranges: str) -> Iterable[VidRange]:
|
|
32
|
+
for range in ranges.split(","):
|
|
33
|
+
start, sep, stop = range.strip().partition("-")
|
|
34
|
+
try:
|
|
35
|
+
if not sep:
|
|
36
|
+
int_start = int(start)
|
|
37
|
+
yield VidRange(int_start, int_start)
|
|
38
|
+
elif not stop or not start:
|
|
39
|
+
raise ValueError(f"Cannot parse range {range!r}. Expected `start-stop`")
|
|
40
|
+
else:
|
|
41
|
+
yield VidRange(int(start), int(stop))
|
|
42
|
+
except ValueError:
|
|
43
|
+
raise ValueError(f"Cannot parse range {range!r}. Expected `vid1-vid2` or `vid`")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class VidCollection:
|
|
47
|
+
@staticmethod
|
|
48
|
+
def parse(ranges: int | str) -> "VidCollection":
|
|
49
|
+
if isinstance(ranges, int):
|
|
50
|
+
return VidCollection([VidRange(ranges, ranges)])
|
|
51
|
+
elif isinstance(ranges, str):
|
|
52
|
+
return VidCollection(list(_parse_vlan_ranges(ranges)))
|
|
53
|
+
elif isinstance(ranges, VidCollection):
|
|
54
|
+
return VidCollection(ranges.ranges)
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError(f"Expected str or int, got {type(ranges)}")
|
|
57
|
+
|
|
58
|
+
def __init__(self, ranges: list[VidRange]) -> None:
|
|
59
|
+
self.ranges = ranges
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return ",".join(map(str, self.ranges))
|
|
63
|
+
|
|
64
|
+
def __repr__(self):
|
|
65
|
+
return f"VlanCollection({str(self)!r})"
|
|
66
|
+
|
|
67
|
+
def __iter__(self):
|
|
68
|
+
for range in self.ranges:
|
|
69
|
+
yield from range
|
|
70
|
+
|
|
71
|
+
def __eq__(self, other: object) -> bool:
|
|
72
|
+
if type(other) is VidCollection:
|
|
73
|
+
return self.ranges == other.ranges
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
6
77
|
class ASN(int):
|
|
7
78
|
"""
|
|
8
79
|
Stores ASN number and formats it as в AS1.AS2
|
|
@@ -182,6 +253,7 @@ class PeerGroup:
|
|
|
182
253
|
internal_name: str = ""
|
|
183
254
|
description: str = ""
|
|
184
255
|
update_source: str = ""
|
|
256
|
+
peer_filter: str = ""
|
|
185
257
|
import_policy: str = ""
|
|
186
258
|
export_policy: str = ""
|
|
187
259
|
|
|
@@ -234,6 +306,17 @@ class PeerGroup:
|
|
|
234
306
|
mtu: int = 0
|
|
235
307
|
|
|
236
308
|
|
|
309
|
+
@dataclass
|
|
310
|
+
class L2VpnOptions:
|
|
311
|
+
name: str
|
|
312
|
+
vid: VidCollection
|
|
313
|
+
l2vni: int # VNI, possible values are 1 to 2**24-1
|
|
314
|
+
route_distinguisher: str = "" # like in VrfOptions
|
|
315
|
+
rt_import: list[str] = field(default_factory=list) # like in VrfOptions
|
|
316
|
+
rt_export: list[str] = field(default_factory=list) # like in VrfOptions
|
|
317
|
+
advertise_host_routes: bool = True # advertise IP+MAC routes into L3VNI
|
|
318
|
+
|
|
319
|
+
|
|
237
320
|
@dataclass
|
|
238
321
|
class VrfOptions:
|
|
239
322
|
vrf_name: str
|
|
@@ -273,8 +356,8 @@ class GlobalOptions:
|
|
|
273
356
|
multipath: int = 0
|
|
274
357
|
router_id: str = ""
|
|
275
358
|
vrf: dict[str, VrfOptions] = field(default_factory=dict)
|
|
276
|
-
|
|
277
359
|
groups: list[PeerGroup] = field(default_factory=list)
|
|
360
|
+
l2vpn: dict[str, L2VpnOptions] = field(default_factory=dict)
|
|
278
361
|
|
|
279
362
|
|
|
280
363
|
@dataclass
|
|
@@ -111,33 +111,30 @@ class AdapterWithName(ABC):
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def get_connector_from_config(config_key: str, connectors: List[Type[Connector]]) -> Tuple[Connector, Dict[str, Any]]:
|
|
114
|
-
seen: list[str] = []
|
|
115
114
|
if not connectors:
|
|
116
115
|
raise Exception("empty connectors")
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
|
|
117
|
+
# handle configuration
|
|
118
|
+
connector_params = dict[str, Any]() # default
|
|
119
119
|
if context_storage := get_context().get(config_key):
|
|
120
|
-
adapter_name = context_storage.get("adapter", None)
|
|
121
120
|
connector_params = context_storage.get("params", {})
|
|
122
|
-
if adapter_name:
|
|
121
|
+
if adapter_name := context_storage.get("adapter", None):
|
|
122
|
+
seen = list[str]()
|
|
123
123
|
for con in connectors:
|
|
124
|
-
con_name = connector.__name__
|
|
125
124
|
if issubclass(con, AdapterWithName):
|
|
126
125
|
con_name = con.name()
|
|
126
|
+
else:
|
|
127
|
+
con_name = con.__name__
|
|
127
128
|
seen.append(con_name)
|
|
128
129
|
if adapter_name == con_name:
|
|
129
|
-
|
|
130
|
+
connectors = [con]
|
|
130
131
|
break
|
|
131
132
|
else:
|
|
132
133
|
raise Exception("unknown %s %s: seen %s" % (config_key, adapter_name, seen))
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
else:
|
|
138
|
-
connector = connectors[0]
|
|
139
|
-
if len(connectors) > 1:
|
|
140
|
-
warnings.warn(f"Please specify '{config_key}'. Found more than one classes {connectors}", UserWarning)
|
|
134
|
+
|
|
135
|
+
if len(connectors) > 1:
|
|
136
|
+
warnings.warn(f"Please specify adapter for '{config_key}'. Found more than one classes {connectors}", UserWarning)
|
|
137
|
+
connector = connectors[0]
|
|
141
138
|
if issubclass(connector, AdapterWithConfig):
|
|
142
139
|
connector_ins = connector.with_config(**connector_params)
|
|
143
140
|
else:
|
|
@@ -76,10 +76,21 @@ class VrfOptions(_FamiliesMixin, BaseMeshModel):
|
|
|
76
76
|
groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
class L2VpnOptions(BaseMeshModel):
|
|
80
|
+
name: str
|
|
81
|
+
vid: str | int # VLAN ID, possible values are 1 to 4094, ranges can be set as strings
|
|
82
|
+
l2vni: int # VNI, possible values are 1 to 2**24-1
|
|
83
|
+
route_distinguisher: str # like in VrfOptions
|
|
84
|
+
rt_import: Annotated[tuple[str, ...], Concat()] # like in VrfOptions
|
|
85
|
+
rt_export: Annotated[tuple[str, ...], Concat()] # like in VrfOptions
|
|
86
|
+
advertise_host_routes: bool # advertise IP+MAC routes into L3VNI
|
|
87
|
+
|
|
88
|
+
|
|
79
89
|
class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
|
|
80
90
|
def __init__(self, **kwargs):
|
|
81
91
|
kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
|
|
82
92
|
kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
|
|
93
|
+
kwargs.setdefault("l2vpn", KeyDefaultDict(lambda x: L2VpnOptions(name=x)))
|
|
83
94
|
super().__init__(**kwargs)
|
|
84
95
|
|
|
85
96
|
as_path_relax: bool
|
|
@@ -89,3 +100,4 @@ class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
|
|
|
89
100
|
router_id: str
|
|
90
101
|
vrf: Annotated[dict[str, VrfOptions], DictMerge(Merge())]
|
|
91
102
|
groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
|
|
103
|
+
l2vpn: Annotated[dict[str, L2VpnOptions], DictMerge(Merge())]
|
|
@@ -132,30 +132,30 @@ class MeshExecutor:
|
|
|
132
132
|
# we can have multiple rules for the same pair
|
|
133
133
|
# we merge them according to remote fqdn
|
|
134
134
|
neighbor_peers: dict[PeerKey, Pair] = {}
|
|
135
|
-
# TODO batch resolve
|
|
136
135
|
rules = self._registry.lookup_direct(device.fqdn, device.neighbours_fqdns)
|
|
137
136
|
fqdns = {
|
|
138
137
|
rule.name_right if rule.direct_order else rule.name_left
|
|
139
138
|
for rule in rules
|
|
140
139
|
}
|
|
141
|
-
|
|
140
|
+
logger.debug("Loading neighbor devices: %s", fqdns)
|
|
141
|
+
neighbors = {
|
|
142
142
|
d.fqdn: d for d in self._storage.make_devices(list(fqdns))
|
|
143
143
|
}
|
|
144
144
|
for rule in rules:
|
|
145
145
|
handler_name = self._handler_name(rule.handler)
|
|
146
146
|
if rule.direct_order:
|
|
147
|
-
if rule.name_right not in
|
|
148
|
-
print(list(neigbors), flush=True)
|
|
147
|
+
if rule.name_right not in neighbors:
|
|
149
148
|
raise ValueError(
|
|
150
|
-
f"Device `{device.fqdn}` has no neighbor `{rule.name_right}`
|
|
149
|
+
f"Device `{device.fqdn}` has no neighbor `{rule.name_right}` "
|
|
150
|
+
f"required for `{handler_name}`. {list(neighbors)}",
|
|
151
151
|
)
|
|
152
|
-
neighbor_device =
|
|
152
|
+
neighbor_device = neighbors[rule.name_right]
|
|
153
153
|
else:
|
|
154
|
-
if rule.name_left not in
|
|
154
|
+
if rule.name_left not in neighbors:
|
|
155
155
|
raise ValueError(
|
|
156
156
|
f"Device `{device.fqdn}` has no neighbor `{rule.name_left}` required for `{handler_name}`",
|
|
157
157
|
)
|
|
158
|
-
neighbor_device =
|
|
158
|
+
neighbor_device = neighbors[rule.name_left]
|
|
159
159
|
all_connected_ports = [
|
|
160
160
|
(p1.name, p2.name)
|
|
161
161
|
for p1, p2 in self._storage.search_connections(device, neighbor_device)
|
|
@@ -238,6 +238,7 @@ class MeshExecutor:
|
|
|
238
238
|
rule.name_right if rule.direct_order else rule.name_left
|
|
239
239
|
for rule in rules
|
|
240
240
|
}
|
|
241
|
+
logger.debug("Loading indirect connected devices: %s", fqdns)
|
|
241
242
|
connected_devices = {
|
|
242
243
|
d.fqdn: d for d in self._storage.make_devices(list(fqdns))
|
|
243
244
|
}
|
|
@@ -7,7 +7,7 @@ from adaptix import Retort, loader, Chain, name_mapping, as_is_loader
|
|
|
7
7
|
from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO, VirtualLocalDTO
|
|
8
8
|
from ..bgp_models import (
|
|
9
9
|
Aggregate, GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions,
|
|
10
|
-
Redistribute, BFDTimers,
|
|
10
|
+
Redistribute, BFDTimers, L2VpnOptions, VidCollection,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
|
|
@@ -49,8 +49,10 @@ retort = Retort(
|
|
|
49
49
|
recipe=[
|
|
50
50
|
loader(InterfaceChanges, ObjMapping, Chain.FIRST),
|
|
51
51
|
loader(ASN, ASN),
|
|
52
|
+
loader(VidCollection, VidCollection.parse),
|
|
52
53
|
loader(GlobalOptions, ObjMapping, Chain.FIRST),
|
|
53
54
|
loader(VrfOptions, ObjMapping, Chain.FIRST),
|
|
55
|
+
loader(L2VpnOptions, ObjMapping, Chain.FIRST),
|
|
54
56
|
loader(FamilyOptions, ObjMapping, Chain.FIRST),
|
|
55
57
|
loader(Aggregate, ObjMapping, Chain.FIRST),
|
|
56
58
|
loader(PeerOptions, ObjMapping, Chain.FIRST),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from annet.annlib.types import Op
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class UserConfig:
|
|
8
|
+
name: str
|
|
9
|
+
privilege: int
|
|
10
|
+
role: str
|
|
11
|
+
secret_type: str
|
|
12
|
+
secret: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_user_config(config_line):
|
|
16
|
+
"""Convert a user config line into a dataclass. Config example:
|
|
17
|
+
|
|
18
|
+
username someuser privilege 15 role network-admin secret sha512 $6$....
|
|
19
|
+
|
|
20
|
+
privilege could be omitted if equal to 1
|
|
21
|
+
role could be omitted
|
|
22
|
+
secret could be omitted, 'nopassword' is provided instead
|
|
23
|
+
"""
|
|
24
|
+
splstr = config_line.split()
|
|
25
|
+
name = splstr[1]
|
|
26
|
+
priv = 1
|
|
27
|
+
role = ""
|
|
28
|
+
secret_type = ""
|
|
29
|
+
secret = ""
|
|
30
|
+
if "privilege" in splstr:
|
|
31
|
+
pos = splstr.index("privilege")
|
|
32
|
+
priv = int(splstr[pos + 1])
|
|
33
|
+
if "role" in splstr:
|
|
34
|
+
pos = splstr.index("role")
|
|
35
|
+
role = splstr[pos + 1]
|
|
36
|
+
if "secret" in splstr:
|
|
37
|
+
pos = splstr.index("secret")
|
|
38
|
+
secret_type = splstr[pos + 1]
|
|
39
|
+
secret = splstr[pos + 2]
|
|
40
|
+
return UserConfig(name=name, privilege=priv, role=role, secret_type=secret_type, secret=secret)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def user(key, diff, **_):
|
|
44
|
+
if diff[Op.ADDED] and not diff[Op.REMOVED]:
|
|
45
|
+
for add in diff[Op.ADDED]:
|
|
46
|
+
yield (True, add["row"], None)
|
|
47
|
+
elif diff[Op.REMOVED] and not diff[Op.ADDED]:
|
|
48
|
+
for _ in diff[Op.REMOVED]:
|
|
49
|
+
yield (False, f"no username {key[0]}", None)
|
|
50
|
+
else:
|
|
51
|
+
for num, add in enumerate(diff[Op.ADDED]):
|
|
52
|
+
new_user = _parse_user_config(add["row"])
|
|
53
|
+
old_user = _parse_user_config(diff[Op.REMOVED][num]["row"])
|
|
54
|
+
if new_user.privilege != old_user.privilege:
|
|
55
|
+
yield (True, f"username {key[0]} privilege {new_user.privilege}", None)
|
|
56
|
+
if new_user.role != old_user.role and not new_user.role:
|
|
57
|
+
yield (True, f"no username {key[0]} role", None)
|
|
58
|
+
elif new_user.role != old_user.role:
|
|
59
|
+
yield (True, f"username {key[0]} role {new_user.role}", None)
|
|
60
|
+
if new_user.secret != old_user.secret and not new_user.secret:
|
|
61
|
+
yield (True, f"username {key[0]} nopassword", None)
|
|
62
|
+
elif new_user.secret != old_user.secret:
|
|
63
|
+
yield (True, f"username {key[0]} secret {new_user.secret_type} {new_user.secret}", None)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: annet
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: annet
|
|
5
5
|
Home-page: https://github.com/annetutil/annet
|
|
6
6
|
License: MIT
|
|
@@ -23,7 +23,7 @@ Requires-Dist: yarl>=1.8.2
|
|
|
23
23
|
Requires-Dist: adaptix==3.0.0b7
|
|
24
24
|
Requires-Dist: dataclass-rest==0.4
|
|
25
25
|
Provides-Extra: netbox
|
|
26
|
-
Requires-Dist: annetbox[sync]>=0.
|
|
26
|
+
Requires-Dist: annetbox[sync]>=0.2.0; extra == "netbox"
|
|
27
27
|
Dynamic: home-page
|
|
28
28
|
Dynamic: license
|
|
29
29
|
Dynamic: provides-extra
|
|
@@ -125,6 +125,7 @@ annet/rulebook/common.py
|
|
|
125
125
|
annet/rulebook/deploying.py
|
|
126
126
|
annet/rulebook/patching.py
|
|
127
127
|
annet/rulebook/arista/__init__.py
|
|
128
|
+
annet/rulebook/arista/aaa.py
|
|
128
129
|
annet/rulebook/arista/iface.py
|
|
129
130
|
annet/rulebook/aruba/__init__.py
|
|
130
131
|
annet/rulebook/aruba/ap_env.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|