annet 0.6__tar.gz → 0.8__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.6/annet.egg-info → annet-0.8}/PKG-INFO +3 -1
- {annet-0.6 → annet-0.8}/README.md +9 -0
- annet-0.8/annet/adapters/netbox/common/client.py +87 -0
- annet-0.8/annet/adapters/netbox/common/manufacturer.py +62 -0
- annet-0.8/annet/adapters/netbox/common/models.py +98 -0
- annet-0.8/annet/adapters/netbox/common/query.py +23 -0
- annet-0.8/annet/adapters/netbox/common/status_client.py +24 -0
- annet-0.8/annet/adapters/netbox/common/storage_opts.py +14 -0
- annet-0.8/annet/adapters/netbox/provider.py +34 -0
- annet-0.8/annet/adapters/netbox/v24/api_models.py +72 -0
- annet-0.8/annet/adapters/netbox/v24/client.py +59 -0
- annet-0.8/annet/adapters/netbox/v24/storage.py +190 -0
- annet-0.8/annet/adapters/netbox/v37/api_models.py +37 -0
- annet-0.8/annet/adapters/netbox/v37/client.py +62 -0
- annet-0.8/annet/adapters/netbox/v37/storage.py +143 -0
- {annet-0.6 → annet-0.8}/annet/annlib/jsontools.py +23 -0
- {annet-0.6 → annet-0.8}/annet/api/__init__.py +18 -6
- {annet-0.6 → annet-0.8}/annet/cli.py +6 -2
- {annet-0.6 → annet-0.8}/annet/cli_args.py +10 -0
- {annet-0.6 → annet-0.8}/annet/diff.py +1 -2
- {annet-0.6 → annet-0.8}/annet/gen.py +34 -4
- {annet-0.6 → annet-0.8}/annet/generators/__init__.py +78 -67
- {annet-0.6 → annet-0.8}/annet/output.py +3 -1
- annet-0.8/annet/rulebook/arista/__init__.py +0 -0
- annet-0.8/annet/rulebook/cisco/__init__.py +0 -0
- annet-0.8/annet/rulebook/huawei/__init__.py +0 -0
- annet-0.8/annet/rulebook/nexus/__init__.py +0 -0
- {annet-0.6 → annet-0.8/annet.egg-info}/PKG-INFO +3 -1
- {annet-0.6 → annet-0.8}/annet.egg-info/SOURCES.txt +19 -3
- annet-0.8/annet.egg-info/entry_points.txt +5 -0
- {annet-0.6 → annet-0.8}/annet.egg-info/requires.txt +2 -0
- {annet-0.6 → annet-0.8}/annet.egg-info/top_level.txt +0 -1
- annet-0.8/annet_generators/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/requirements.txt +2 -0
- {annet-0.6 → annet-0.8}/setup.py +1 -4
- annet-0.6/annet.egg-info/entry_points.txt +0 -6
- annet-0.6/annet_nbexport/__init__.py +0 -220
- annet-0.6/annet_nbexport/main.py +0 -46
- {annet-0.6 → annet-0.8}/AUTHORS +0 -0
- {annet-0.6 → annet-0.8}/LICENSE +0 -0
- {annet-0.6 → annet-0.8}/MANIFEST.in +0 -0
- {annet-0.6 → annet-0.8}/annet/__init__.py +0 -0
- {annet-0.6/annet/annlib/netdev → annet-0.8/annet/adapters}/__init__.py +0 -0
- {annet-0.6/annet/annlib/netdev/views → annet-0.8/annet/adapters/netbox}/__init__.py +0 -0
- {annet-0.6/annet/annlib/rbparser → annet-0.8/annet/adapters/netbox/common}/__init__.py +0 -0
- {annet-0.6/annet/annlib/rulebook → annet-0.8/annet/adapters/netbox/v24}/__init__.py +0 -0
- {annet-0.6/annet/generators/common → annet-0.8/annet/adapters/netbox/v37}/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annet.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/command.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/diff.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/errors.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/filter_acl.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/lib.py +0 -0
- {annet-0.6/annet/rulebook/arista → annet-0.8/annet/annlib/netdev}/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/netdev/db.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/netdev/devdb/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/netdev/devdb/data/devdb.json +0 -0
- {annet-0.6/annet/rulebook/cisco → annet-0.8/annet/annlib/netdev/views}/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/netdev/views/dump.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/netdev/views/hardware.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/output.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/patching.py +0 -0
- {annet-0.6/annet/rulebook/huawei → annet-0.8/annet/annlib/rbparser}/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/rbparser/acl.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/rbparser/deploying.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/rbparser/ordering.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/rbparser/platform.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/rbparser/syntax.py +0 -0
- {annet-0.6/annet/rulebook/nexus → annet-0.8/annet/annlib/rulebook}/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/rulebook/common.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/tabparser.py +0 -0
- {annet-0.6 → annet-0.8}/annet/annlib/types.py +0 -0
- {annet-0.6 → annet-0.8}/annet/argparse.py +0 -0
- {annet-0.6 → annet-0.8}/annet/configs/context.yml +0 -0
- {annet-0.6 → annet-0.8}/annet/configs/logging.yaml +0 -0
- {annet-0.6 → annet-0.8}/annet/connectors.py +0 -0
- {annet-0.6 → annet-0.8}/annet/deploy.py +0 -0
- {annet-0.6 → annet-0.8}/annet/executor.py +0 -0
- {annet-0.6 → annet-0.8}/annet/filtering.py +0 -0
- {annet-0.6/annet_generators → annet-0.8/annet/generators/common}/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/generators/common/initial.py +0 -0
- {annet-0.6 → annet-0.8}/annet/hardware.py +0 -0
- {annet-0.6 → annet-0.8}/annet/implicit.py +0 -0
- {annet-0.6 → annet-0.8}/annet/lib.py +0 -0
- {annet-0.6 → annet-0.8}/annet/parallel.py +0 -0
- {annet-0.6 → annet-0.8}/annet/patching.py +0 -0
- {annet-0.6 → annet-0.8}/annet/reference.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/arista/iface.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/aruba/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/aruba/ap_env.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/aruba/misc.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/cisco/iface.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/cisco/misc.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/cisco/vlandb.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/common.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/deploying.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/huawei/aaa.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/huawei/bgp.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/huawei/iface.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/huawei/misc.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/huawei/vlandb.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/juniper/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/nexus/iface.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/patching.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/ribbon/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/arista.deploy +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/arista.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/arista.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/aruba.deploy +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/aruba.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/aruba.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/cisco.deploy +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/cisco.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/cisco.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/huawei.deploy +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/huawei.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/huawei.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/juniper.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/nexus.deploy +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/nexus.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/nexus.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/nokia.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/pc.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/pc.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/ribbon.deploy +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/ribbon.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/routeros.order +0 -0
- {annet-0.6 → annet-0.8}/annet/rulebook/texts/routeros.rul +0 -0
- {annet-0.6 → annet-0.8}/annet/storage.py +0 -0
- {annet-0.6 → annet-0.8}/annet/tabparser.py +0 -0
- {annet-0.6 → annet-0.8}/annet/text_term_format.py +0 -0
- {annet-0.6 → annet-0.8}/annet/tracing.py +0 -0
- {annet-0.6 → annet-0.8}/annet/types.py +0 -0
- {annet-0.6 → annet-0.8}/annet.egg-info/dependency_links.txt +0 -0
- {annet-0.6 → annet-0.8}/annet_generators/example/__init__.py +0 -0
- {annet-0.6 → annet-0.8}/annet_generators/example/lldp.py +0 -0
- {annet-0.6 → annet-0.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: annet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8
|
|
4
4
|
Summary: annet
|
|
5
5
|
Home-page: https://github.com/annetutil/annet
|
|
6
6
|
License: MIT
|
|
@@ -21,3 +21,5 @@ Requires-Dist: contextlog>=1.1
|
|
|
21
21
|
Requires-Dist: valkit>=0.1.4
|
|
22
22
|
Requires-Dist: aiohttp>=3.8.4
|
|
23
23
|
Requires-Dist: yarl>=1.8.2
|
|
24
|
+
Requires-Dist: adaptix==3.0.0b2
|
|
25
|
+
Requires-Dist: dataclass-rest==0.4
|
|
@@ -13,6 +13,15 @@ Usage help can be obtained by calling ```annet -h``` or for a specific command,
|
|
|
13
13
|
|
|
14
14
|
## Overview
|
|
15
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
|
+
|
|
16
25
|
### annet gen
|
|
17
26
|
|
|
18
27
|
The annet_generators directory contains many files called generators.
|
|
@@ -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,98 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any
|
|
4
|
+
|
|
5
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Entity:
|
|
10
|
+
id: int
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Label:
|
|
16
|
+
value: str
|
|
17
|
+
label: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class IpFamily:
|
|
22
|
+
value: int
|
|
23
|
+
label: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class DeviceType:
|
|
28
|
+
id: int
|
|
29
|
+
manufacturer: Entity
|
|
30
|
+
model: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DeviceIp:
|
|
35
|
+
id: int
|
|
36
|
+
display: str
|
|
37
|
+
address: str
|
|
38
|
+
family: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class IpAddress:
|
|
43
|
+
id: int
|
|
44
|
+
assigned_object_id: int
|
|
45
|
+
display: str
|
|
46
|
+
family: IpFamily
|
|
47
|
+
address: str
|
|
48
|
+
status: Label
|
|
49
|
+
tags: List[Entity]
|
|
50
|
+
created: datetime
|
|
51
|
+
last_updated: datetime
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Interface(Entity):
|
|
56
|
+
device: Entity
|
|
57
|
+
enabled: bool
|
|
58
|
+
display: str = ""
|
|
59
|
+
ip_addresses: List[IpAddress] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class NetboxDevice(Entity):
|
|
64
|
+
neighbours_ids: List[int]
|
|
65
|
+
|
|
66
|
+
display: str
|
|
67
|
+
device_type: DeviceType
|
|
68
|
+
device_role: Entity
|
|
69
|
+
tenant: Optional[Entity]
|
|
70
|
+
platform: Optional[Entity]
|
|
71
|
+
serial: str
|
|
72
|
+
asset_tag: Optional[str]
|
|
73
|
+
site: Entity
|
|
74
|
+
rack: Optional[Entity]
|
|
75
|
+
position: Optional[float]
|
|
76
|
+
face: Optional[Label]
|
|
77
|
+
status: Label
|
|
78
|
+
primary_ip: Optional[DeviceIp]
|
|
79
|
+
primary_ip4: Optional[DeviceIp]
|
|
80
|
+
primary_ip6: Optional[DeviceIp]
|
|
81
|
+
tags: List[Entity]
|
|
82
|
+
custom_fields: dict[str, Any]
|
|
83
|
+
created: datetime
|
|
84
|
+
last_updated: datetime
|
|
85
|
+
|
|
86
|
+
fqdn: str
|
|
87
|
+
hostname: str
|
|
88
|
+
hw: Optional[HardwareView]
|
|
89
|
+
breed: str
|
|
90
|
+
|
|
91
|
+
interfaces: List[Interface]
|
|
92
|
+
|
|
93
|
+
# compat
|
|
94
|
+
def __hash__(self):
|
|
95
|
+
return hash(self.id)
|
|
96
|
+
|
|
97
|
+
def is_pc(self):
|
|
98
|
+
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,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from adaptix import Retort, name_mapping, NameStyle
|
|
4
|
+
from dataclass_rest import get
|
|
5
|
+
from dataclass_rest.client_protocol import FactoryProtocol
|
|
6
|
+
|
|
7
|
+
from .client import BaseNetboxClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Status:
|
|
12
|
+
netbox_version: str
|
|
13
|
+
plugins: dict[str, str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NetboxStatusClient(BaseNetboxClient):
|
|
17
|
+
def _init_response_body_factory(self) -> FactoryProtocol:
|
|
18
|
+
return Retort(recipe=[
|
|
19
|
+
name_mapping(name_style=NameStyle.LOWER_KEBAB)
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
@get("status")
|
|
23
|
+
def status(self) -> Status:
|
|
24
|
+
...
|
|
@@ -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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any
|
|
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
|
+
display_name: str
|
|
24
|
+
device_type: DeviceType
|
|
25
|
+
device_role: Entity
|
|
26
|
+
tenant: Optional[Entity]
|
|
27
|
+
platform: Optional[Entity]
|
|
28
|
+
serial: str
|
|
29
|
+
asset_tag: Optional[str]
|
|
30
|
+
site: Entity
|
|
31
|
+
rack: Optional[Entity]
|
|
32
|
+
position: Optional[float]
|
|
33
|
+
face: Optional[Label]
|
|
34
|
+
status: Label
|
|
35
|
+
primary_ip: Optional[DeviceIp]
|
|
36
|
+
primary_ip4: Optional[DeviceIp]
|
|
37
|
+
primary_ip6: Optional[DeviceIp]
|
|
38
|
+
tags: List[str]
|
|
39
|
+
custom_fields: dict[str, Any]
|
|
40
|
+
created: datetime
|
|
41
|
+
last_updated: datetime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Interface(Entity):
|
|
46
|
+
device: Entity
|
|
47
|
+
enabled: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Vrf(Entity):
|
|
52
|
+
rd: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class IpAddress:
|
|
57
|
+
id: int
|
|
58
|
+
family: int
|
|
59
|
+
address: str
|
|
60
|
+
vrf: Optional[Vrf]
|
|
61
|
+
tenant: Any # ???
|
|
62
|
+
status: Label
|
|
63
|
+
description: Optional[str]
|
|
64
|
+
custom_fields: dict[str, Any]
|
|
65
|
+
tags: List[str]
|
|
66
|
+
created: datetime
|
|
67
|
+
last_updated: datetime
|
|
68
|
+
|
|
69
|
+
interface: Entity
|
|
70
|
+
|
|
71
|
+
nat_inside: Any # ???
|
|
72
|
+
nat_outside: Any # ???
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import dateutil.parser
|
|
5
|
+
from adaptix import Retort, loader
|
|
6
|
+
from dataclass_rest import get
|
|
7
|
+
|
|
8
|
+
from annet.adapters.netbox.common.client import (
|
|
9
|
+
BaseNetboxClient, collect, PagingResponse,
|
|
10
|
+
)
|
|
11
|
+
from .api_models import Device, Interface, IpAddress
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NetboxV24(BaseNetboxClient):
|
|
15
|
+
def _init_response_body_factory(self) -> Retort:
|
|
16
|
+
return Retort(recipe=[
|
|
17
|
+
loader(datetime, dateutil.parser.parse)
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
@get("dcim/interfaces")
|
|
21
|
+
def interfaces(
|
|
22
|
+
self,
|
|
23
|
+
device_id: Optional[List[int]] = None,
|
|
24
|
+
limit: int = 20,
|
|
25
|
+
offset: int = 0,
|
|
26
|
+
) -> PagingResponse[Interface]:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
all_interfaces = collect(interfaces, field="device_id")
|
|
30
|
+
|
|
31
|
+
@get("ipam/ip-addresses")
|
|
32
|
+
def ip_addresses(
|
|
33
|
+
self,
|
|
34
|
+
interface_id: Optional[List[int]] = None,
|
|
35
|
+
limit: int = 20,
|
|
36
|
+
offset: int = 0,
|
|
37
|
+
) -> PagingResponse[IpAddress]:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
all_ip_addresses = collect(ip_addresses, field="interface_id")
|
|
41
|
+
|
|
42
|
+
@get("dcim/devices")
|
|
43
|
+
def devices(
|
|
44
|
+
self,
|
|
45
|
+
name: Optional[List[str]] = None,
|
|
46
|
+
tag: Optional[str] = None,
|
|
47
|
+
limit: int = 20,
|
|
48
|
+
offset: int = 0,
|
|
49
|
+
) -> PagingResponse[Device]:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
all_devices = collect(devices)
|
|
53
|
+
|
|
54
|
+
@get("dcim/devices/{device_id}")
|
|
55
|
+
def get_device(
|
|
56
|
+
self,
|
|
57
|
+
device_id: int,
|
|
58
|
+
) -> Device:
|
|
59
|
+
pass
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from annet.adapters.netbox.common import models
|
|
5
|
+
from annet.adapters.netbox.common.manufacturer import (
|
|
6
|
+
is_supported, get_hw, get_breed,
|
|
7
|
+
)
|
|
8
|
+
from annet.adapters.netbox.common.query import NetboxQuery
|
|
9
|
+
from annet.adapters.netbox.common.storage_opts import NetboxStorageOpts
|
|
10
|
+
from annet.storage import Storage
|
|
11
|
+
from . import api_models
|
|
12
|
+
from .client import NetboxV24
|
|
13
|
+
|
|
14
|
+
logger = getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extend_device_ip(
|
|
18
|
+
ip: Optional[api_models.DeviceIp],
|
|
19
|
+
) -> Optional[models.DeviceIp]:
|
|
20
|
+
if not ip:
|
|
21
|
+
return None
|
|
22
|
+
return models.DeviceIp(
|
|
23
|
+
address=ip.address,
|
|
24
|
+
id=ip.id,
|
|
25
|
+
display=ip.address,
|
|
26
|
+
family=ip.family,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extend_label(
|
|
31
|
+
label: Optional[api_models.Label],
|
|
32
|
+
) -> Optional[models.Label]:
|
|
33
|
+
if not label:
|
|
34
|
+
return None
|
|
35
|
+
return models.Label(
|
|
36
|
+
label=label.label,
|
|
37
|
+
value=str(label.value),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extend_device(
|
|
42
|
+
device: api_models.Device,
|
|
43
|
+
) -> models.NetboxDevice:
|
|
44
|
+
manufacturer = device.device_type.manufacturer.name
|
|
45
|
+
model = device.device_type.model
|
|
46
|
+
|
|
47
|
+
return models.NetboxDevice(
|
|
48
|
+
id=device.id,
|
|
49
|
+
name=device.name,
|
|
50
|
+
display=device.display_name,
|
|
51
|
+
device_type=device.device_type,
|
|
52
|
+
device_role=device.device_role,
|
|
53
|
+
tenant=device.tenant,
|
|
54
|
+
platform=device.platform,
|
|
55
|
+
serial=device.serial,
|
|
56
|
+
asset_tag=device.asset_tag,
|
|
57
|
+
site=device.site,
|
|
58
|
+
rack=device.rack,
|
|
59
|
+
position=device.position,
|
|
60
|
+
face=extend_label(device.face),
|
|
61
|
+
status=device.status,
|
|
62
|
+
primary_ip=extend_device_ip(device.primary_ip),
|
|
63
|
+
primary_ip4=extend_device_ip(device.primary_ip4),
|
|
64
|
+
primary_ip6=extend_device_ip(device.primary_ip6),
|
|
65
|
+
tags=[models.Entity(0, tag) for tag in device.tags],
|
|
66
|
+
custom_fields=device.custom_fields, # ???
|
|
67
|
+
created=device.created,
|
|
68
|
+
last_updated=device.last_updated,
|
|
69
|
+
|
|
70
|
+
fqdn=device.name,
|
|
71
|
+
hostname=device.name,
|
|
72
|
+
hw=get_hw(manufacturer, model),
|
|
73
|
+
breed=get_breed(manufacturer, model),
|
|
74
|
+
interfaces=[],
|
|
75
|
+
neighbours_ids=[],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extend_interface(interface: api_models.Interface) -> models.Interface:
|
|
80
|
+
return models.Interface(
|
|
81
|
+
id=interface.id,
|
|
82
|
+
name=interface.name,
|
|
83
|
+
device=interface.device,
|
|
84
|
+
enabled=interface.enabled,
|
|
85
|
+
display=interface.name,
|
|
86
|
+
ip_addresses=[],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def extend_ip(ip: api_models.IpAddress) -> models.IpAddress:
|
|
91
|
+
return models.IpAddress(
|
|
92
|
+
id=ip.id,
|
|
93
|
+
assigned_object_id=ip.interface.id,
|
|
94
|
+
display=ip.address,
|
|
95
|
+
family=models.IpFamily(
|
|
96
|
+
value=ip.family,
|
|
97
|
+
label=str(ip.family),
|
|
98
|
+
),
|
|
99
|
+
address=ip.address,
|
|
100
|
+
status=extend_label(ip.status),
|
|
101
|
+
tags=[models.Entity(0, tag) for tag in ip.tags],
|
|
102
|
+
created=ip.created,
|
|
103
|
+
last_updated=ip.last_updated,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class NetboxStorageV24(Storage):
|
|
108
|
+
def __init__(self, opts: Optional[NetboxStorageOpts] = None):
|
|
109
|
+
self.netbox = NetboxV24(
|
|
110
|
+
url=opts.url,
|
|
111
|
+
token=opts.token,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def __enter__(self):
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def __exit__(self, _, __, ___):
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def resolve_object_ids_by_query(self, query: NetboxQuery):
|
|
121
|
+
return [
|
|
122
|
+
d.id for d in self._load_devices(query)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
def resolve_fdnds_by_query(self, query: NetboxQuery):
|
|
126
|
+
return [
|
|
127
|
+
d.name for d in self._load_devices(query)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
def make_devices(
|
|
131
|
+
self,
|
|
132
|
+
query: NetboxQuery,
|
|
133
|
+
preload_neighbors=False,
|
|
134
|
+
use_mesh=None,
|
|
135
|
+
preload_extra_fields=False,
|
|
136
|
+
**kwargs,
|
|
137
|
+
) -> list[models.NetboxDevice]:
|
|
138
|
+
device_ids = {
|
|
139
|
+
device.id: extend_device(device=device)
|
|
140
|
+
for device in self._load_devices(query)
|
|
141
|
+
}
|
|
142
|
+
if not device_ids:
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
interfaces = self._load_interfaces(list(device_ids))
|
|
146
|
+
for interface in interfaces:
|
|
147
|
+
device_ids[interface.device.id].interfaces.append(interface)
|
|
148
|
+
return list(device_ids.values())
|
|
149
|
+
|
|
150
|
+
def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
|
|
151
|
+
return [
|
|
152
|
+
device
|
|
153
|
+
for device in self.netbox.all_devices().results
|
|
154
|
+
if _match_query(query, device)
|
|
155
|
+
if is_supported(device.device_type.manufacturer.name)
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def _load_interfaces(self, device_ids: List[int]) -> List[
|
|
159
|
+
models.Interface]:
|
|
160
|
+
interfaces = self.netbox.all_interfaces(device_id=device_ids)
|
|
161
|
+
extended_ifaces = {
|
|
162
|
+
interface.id: extend_interface(interface)
|
|
163
|
+
for interface in interfaces.results
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ips = self.netbox.all_ip_addresses(interface_id=list(extended_ifaces))
|
|
167
|
+
for ip in ips.results:
|
|
168
|
+
extended_ip = extend_ip(ip)
|
|
169
|
+
interface = extended_ifaces[extended_ip.assigned_object_id]
|
|
170
|
+
interface.ip_addresses.append(extended_ip)
|
|
171
|
+
return list(extended_ifaces.values())
|
|
172
|
+
|
|
173
|
+
def get_device(
|
|
174
|
+
self, obj_id, preload_neighbors=False, use_mesh=None,
|
|
175
|
+
**kwargs,
|
|
176
|
+
) -> models.NetboxDevice:
|
|
177
|
+
device = self.netbox.get_device(obj_id)
|
|
178
|
+
res = extend_device(device=device)
|
|
179
|
+
res.interfaces = self._load_interfaces([device.id])
|
|
180
|
+
return res
|
|
181
|
+
|
|
182
|
+
def flush_perf(self):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _match_query(query: NetboxQuery, device_data: api_models.Device) -> bool:
|
|
187
|
+
for subquery in query.globs:
|
|
188
|
+
if subquery.strip() in device_data.name:
|
|
189
|
+
return True
|
|
190
|
+
return False
|