annet 0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of annet might be problematic. Click here for more details.
- annet/__init__.py +61 -0
- annet/adapters/__init__.py +0 -0
- annet/adapters/netbox/__init__.py +0 -0
- annet/adapters/netbox/common/__init__.py +0 -0
- annet/adapters/netbox/common/client.py +87 -0
- annet/adapters/netbox/common/manufacturer.py +62 -0
- annet/adapters/netbox/common/models.py +105 -0
- annet/adapters/netbox/common/query.py +23 -0
- annet/adapters/netbox/common/status_client.py +25 -0
- annet/adapters/netbox/common/storage_opts.py +14 -0
- annet/adapters/netbox/provider.py +34 -0
- annet/adapters/netbox/v24/__init__.py +0 -0
- annet/adapters/netbox/v24/api_models.py +73 -0
- annet/adapters/netbox/v24/client.py +59 -0
- annet/adapters/netbox/v24/storage.py +196 -0
- annet/adapters/netbox/v37/__init__.py +0 -0
- annet/adapters/netbox/v37/api_models.py +38 -0
- annet/adapters/netbox/v37/client.py +62 -0
- annet/adapters/netbox/v37/storage.py +149 -0
- annet/annet.py +25 -0
- annet/annlib/__init__.py +7 -0
- annet/annlib/command.py +49 -0
- annet/annlib/diff.py +158 -0
- annet/annlib/errors.py +8 -0
- annet/annlib/filter_acl.py +196 -0
- annet/annlib/jsontools.py +116 -0
- annet/annlib/lib.py +495 -0
- annet/annlib/netdev/__init__.py +0 -0
- annet/annlib/netdev/db.py +62 -0
- annet/annlib/netdev/devdb/__init__.py +28 -0
- annet/annlib/netdev/devdb/data/devdb.json +137 -0
- annet/annlib/netdev/views/__init__.py +0 -0
- annet/annlib/netdev/views/dump.py +121 -0
- annet/annlib/netdev/views/hardware.py +112 -0
- annet/annlib/output.py +246 -0
- annet/annlib/patching.py +533 -0
- annet/annlib/rbparser/__init__.py +0 -0
- annet/annlib/rbparser/acl.py +120 -0
- annet/annlib/rbparser/deploying.py +55 -0
- annet/annlib/rbparser/ordering.py +52 -0
- annet/annlib/rbparser/platform.py +51 -0
- annet/annlib/rbparser/syntax.py +115 -0
- annet/annlib/rulebook/__init__.py +0 -0
- annet/annlib/rulebook/common.py +350 -0
- annet/annlib/tabparser.py +648 -0
- annet/annlib/types.py +35 -0
- annet/api/__init__.py +826 -0
- annet/argparse.py +415 -0
- annet/cli.py +237 -0
- annet/cli_args.py +503 -0
- annet/configs/context.yml +18 -0
- annet/configs/logging.yaml +39 -0
- annet/connectors.py +77 -0
- annet/deploy.py +536 -0
- annet/diff.py +84 -0
- annet/executor.py +551 -0
- annet/filtering.py +40 -0
- annet/gen.py +865 -0
- annet/generators/__init__.py +435 -0
- annet/generators/base.py +136 -0
- annet/generators/common/__init__.py +0 -0
- annet/generators/common/initial.py +33 -0
- annet/generators/entire.py +97 -0
- annet/generators/exceptions.py +10 -0
- annet/generators/jsonfragment.py +125 -0
- annet/generators/partial.py +119 -0
- annet/generators/perf.py +79 -0
- annet/generators/ref.py +15 -0
- annet/generators/result.py +127 -0
- annet/hardware.py +45 -0
- annet/implicit.py +139 -0
- annet/lib.py +128 -0
- annet/output.py +167 -0
- annet/parallel.py +448 -0
- annet/patching.py +25 -0
- annet/reference.py +148 -0
- annet/rulebook/__init__.py +114 -0
- annet/rulebook/arista/__init__.py +0 -0
- annet/rulebook/arista/iface.py +16 -0
- annet/rulebook/aruba/__init__.py +16 -0
- annet/rulebook/aruba/ap_env.py +146 -0
- annet/rulebook/aruba/misc.py +8 -0
- annet/rulebook/cisco/__init__.py +0 -0
- annet/rulebook/cisco/iface.py +68 -0
- annet/rulebook/cisco/misc.py +57 -0
- annet/rulebook/cisco/vlandb.py +90 -0
- annet/rulebook/common.py +19 -0
- annet/rulebook/deploying.py +87 -0
- annet/rulebook/huawei/__init__.py +0 -0
- annet/rulebook/huawei/aaa.py +75 -0
- annet/rulebook/huawei/bgp.py +97 -0
- annet/rulebook/huawei/iface.py +33 -0
- annet/rulebook/huawei/misc.py +337 -0
- annet/rulebook/huawei/vlandb.py +115 -0
- annet/rulebook/juniper/__init__.py +107 -0
- annet/rulebook/nexus/__init__.py +0 -0
- annet/rulebook/nexus/iface.py +92 -0
- annet/rulebook/patching.py +143 -0
- annet/rulebook/ribbon/__init__.py +12 -0
- annet/rulebook/texts/arista.deploy +20 -0
- annet/rulebook/texts/arista.order +125 -0
- annet/rulebook/texts/arista.rul +59 -0
- annet/rulebook/texts/aruba.deploy +20 -0
- annet/rulebook/texts/aruba.order +83 -0
- annet/rulebook/texts/aruba.rul +87 -0
- annet/rulebook/texts/cisco.deploy +27 -0
- annet/rulebook/texts/cisco.order +82 -0
- annet/rulebook/texts/cisco.rul +105 -0
- annet/rulebook/texts/huawei.deploy +188 -0
- annet/rulebook/texts/huawei.order +388 -0
- annet/rulebook/texts/huawei.rul +471 -0
- annet/rulebook/texts/juniper.rul +120 -0
- annet/rulebook/texts/nexus.deploy +24 -0
- annet/rulebook/texts/nexus.order +85 -0
- annet/rulebook/texts/nexus.rul +83 -0
- annet/rulebook/texts/nokia.rul +31 -0
- annet/rulebook/texts/pc.order +5 -0
- annet/rulebook/texts/pc.rul +9 -0
- annet/rulebook/texts/ribbon.deploy +22 -0
- annet/rulebook/texts/ribbon.rul +77 -0
- annet/rulebook/texts/routeros.order +38 -0
- annet/rulebook/texts/routeros.rul +45 -0
- annet/storage.py +125 -0
- annet/tabparser.py +36 -0
- annet/text_term_format.py +95 -0
- annet/tracing.py +170 -0
- annet/types.py +227 -0
- annet-0.0.dist-info/AUTHORS +21 -0
- annet-0.0.dist-info/LICENSE +21 -0
- annet-0.0.dist-info/METADATA +26 -0
- annet-0.0.dist-info/RECORD +137 -0
- annet-0.0.dist-info/WHEEL +5 -0
- annet-0.0.dist-info/entry_points.txt +5 -0
- annet-0.0.dist-info/top_level.txt +2 -0
- annet_generators/__init__.py +0 -0
- annet_generators/example/__init__.py +12 -0
- annet_generators/example/lldp.py +53 -0
annet/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
import os
|
|
4
|
+
import pkgutil
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import SUPPRESS, Namespace
|
|
7
|
+
|
|
8
|
+
import colorama
|
|
9
|
+
import yaml
|
|
10
|
+
from annet.annlib.errors import ( # pylint: disable=wrong-import-position
|
|
11
|
+
DeployCancelled,
|
|
12
|
+
ExecError,
|
|
13
|
+
)
|
|
14
|
+
from contextlog import patch_logging, patch_threading
|
|
15
|
+
from valkit.python import valid_logging_level
|
|
16
|
+
|
|
17
|
+
import annet.argparse
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("DeployCancelled", "ExecError")
|
|
21
|
+
|
|
22
|
+
DEBUG2_LEVELV_NUM = 9
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fill_base_args(parser: annet.argparse.ArgParser, pkg_name: str, logging_config: str):
|
|
26
|
+
parser.add_argument("--log-level", default="WARN", type=valid_logging_level,
|
|
27
|
+
help="Уровень детализации логов (DEBUG, DEBUG2 (with comocutor debug), INFO, WARN, CRITICAL)")
|
|
28
|
+
parser.add_argument("--pkg_name", default=pkg_name, help=SUPPRESS)
|
|
29
|
+
parser.add_argument("--logging_config", default=logging_config, help=SUPPRESS)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init_logging(options: Namespace):
|
|
33
|
+
patch_logging()
|
|
34
|
+
patch_threading()
|
|
35
|
+
logging.captureWarnings(True)
|
|
36
|
+
logging_config = yaml.safe_load(pkgutil.get_data(options.pkg_name, options.logging_config))
|
|
37
|
+
if options.log_level is not None:
|
|
38
|
+
logging_config.setdefault("root", {})
|
|
39
|
+
logging_config["root"]["level"] = options.log_level
|
|
40
|
+
logging.addLevelName(DEBUG2_LEVELV_NUM, "DEBUG2")
|
|
41
|
+
logging.config.dictConfig(logging_config)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def init(options: Namespace):
|
|
45
|
+
init_logging(options)
|
|
46
|
+
|
|
47
|
+
# Отключить colorama.init, если стоит env-переменная. Нужно в тестах
|
|
48
|
+
if os.environ.get("ANN_FORCE_COLOR", None) not in [None, "", "0", "no"]:
|
|
49
|
+
colorama.init = lambda *_, **__: None
|
|
50
|
+
colorama.init()
|
|
51
|
+
|
|
52
|
+
# Workaround for Python 3.8.0: https://bugs.python.org/issue38529
|
|
53
|
+
import asyncio.streams
|
|
54
|
+
if hasattr(asyncio.streams.StreamReaderProtocol, "_on_reader_gc"):
|
|
55
|
+
asyncio.streams.StreamReaderProtocol._on_reader_gc = lambda *args, **kwargs: None # pylint: disable=protected-access
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def assert_python_version():
|
|
59
|
+
if sys.version_info < (3, 8, 0):
|
|
60
|
+
sys.stderr.write("Error: you need python 3.8.0 or higher\n")
|
|
61
|
+
sys.exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Generic, Optional, List, TypeVar, Callable
|
|
4
|
+
|
|
5
|
+
from dataclass_rest.http.requests import RequestsClient
|
|
6
|
+
from requests import Session
|
|
7
|
+
|
|
8
|
+
Model = TypeVar("Model")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PagingResponse(Generic[Model]):
|
|
13
|
+
next: Optional[str]
|
|
14
|
+
previous: Optional[str]
|
|
15
|
+
count: int
|
|
16
|
+
results: List[Model]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Func = TypeVar("Func", bound=Callable)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _collect_by_pages(func: Func) -> Func:
|
|
23
|
+
"""Collect all results using only pagination."""
|
|
24
|
+
@wraps(func)
|
|
25
|
+
def wrapper(self, *args, **kwargs):
|
|
26
|
+
kwargs.setdefault("offset", 0)
|
|
27
|
+
limit = kwargs.setdefault("limit", 100)
|
|
28
|
+
results = []
|
|
29
|
+
method = func.__get__(self, self.__class__)
|
|
30
|
+
has_next = True
|
|
31
|
+
while has_next:
|
|
32
|
+
page = method(*args, **kwargs)
|
|
33
|
+
kwargs["offset"] += limit
|
|
34
|
+
results.extend(page.results)
|
|
35
|
+
has_next = bool(page.next)
|
|
36
|
+
return PagingResponse(
|
|
37
|
+
previous=None,
|
|
38
|
+
next=None,
|
|
39
|
+
count=len(results),
|
|
40
|
+
results=results,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return wrapper
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# default batch size 100 is calculated to fit list of UUIDs in 4k URL length
|
|
47
|
+
def collect(func: Func, field: str = "", batch_size: int = 100) -> Func:
|
|
48
|
+
"""
|
|
49
|
+
Collect data from method iterating over pages and filter batches.
|
|
50
|
+
|
|
51
|
+
:param func: Method to call
|
|
52
|
+
:param field: Field which defines a filter split into batches
|
|
53
|
+
:param batch_size: Limit of values in `field` filter requested at a time
|
|
54
|
+
"""
|
|
55
|
+
func = _collect_by_pages(func)
|
|
56
|
+
if not field:
|
|
57
|
+
return func
|
|
58
|
+
|
|
59
|
+
@wraps(func)
|
|
60
|
+
def wrapper(self, *args, **kwargs):
|
|
61
|
+
value = kwargs.get(field)
|
|
62
|
+
if not value:
|
|
63
|
+
return func(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
method = func.__get__(self, self.__class__)
|
|
66
|
+
results = []
|
|
67
|
+
for offset in range(0, len(value), batch_size):
|
|
68
|
+
kwargs[field] = value[offset:offset + batch_size]
|
|
69
|
+
page = method(*args, **kwargs)
|
|
70
|
+
results.extend(page.results)
|
|
71
|
+
return PagingResponse(
|
|
72
|
+
previous=None,
|
|
73
|
+
next=None,
|
|
74
|
+
count=len(results),
|
|
75
|
+
results=results,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return wrapper
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class BaseNetboxClient(RequestsClient):
|
|
82
|
+
def __init__(self, url: str, token: str):
|
|
83
|
+
url = url.rstrip("/") + "/api/"
|
|
84
|
+
session = Session()
|
|
85
|
+
if token:
|
|
86
|
+
session.headers["Authorization"] = f"Token {token}"
|
|
87
|
+
super().__init__(url, session)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
|
|
3
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
4
|
+
|
|
5
|
+
logger = getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
_VENDORS = {
|
|
8
|
+
"cisco": "Cisco",
|
|
9
|
+
"catalyst": "Cisco Catalyst",
|
|
10
|
+
"nexus": "Cisco Nexus",
|
|
11
|
+
"huawei": "Huawei",
|
|
12
|
+
"juniper": "Juniper",
|
|
13
|
+
"arista": "Arista",
|
|
14
|
+
"pc": "PC",
|
|
15
|
+
"nokia": "Nokia",
|
|
16
|
+
"aruba": "Aruba",
|
|
17
|
+
"routeros": "RouterOS",
|
|
18
|
+
"ribbon": "Ribbon",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _vendor_to_hw(vendor):
|
|
23
|
+
return HardwareView(_VENDORS.get(vendor.lower(), vendor), None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_hw(manufacturer: str, model: str):
|
|
27
|
+
# by some reason Netbox calls Mellanox SN as MSN, so we fix them here
|
|
28
|
+
if manufacturer == "Mellanox" and model.startswith("MSN"):
|
|
29
|
+
model = model.replace("MSN", "SN", 1)
|
|
30
|
+
hw = _vendor_to_hw(manufacturer + " " + model)
|
|
31
|
+
if not hw:
|
|
32
|
+
raise ValueError(f"unsupported manufacturer {manufacturer}")
|
|
33
|
+
return hw
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_breed(manufacturer: str, model: str):
|
|
37
|
+
if manufacturer == "Huawei" and model.startswith("CE"):
|
|
38
|
+
return "vrp85"
|
|
39
|
+
elif manufacturer == "Huawei" and model.startswith("NE"):
|
|
40
|
+
return "vrp85"
|
|
41
|
+
elif manufacturer == "Huawei":
|
|
42
|
+
return "vrp55"
|
|
43
|
+
elif manufacturer == "Mellanox":
|
|
44
|
+
return "cuml2"
|
|
45
|
+
elif manufacturer == "Juniper":
|
|
46
|
+
return "jun10"
|
|
47
|
+
elif manufacturer == "Cisco":
|
|
48
|
+
return "ios12"
|
|
49
|
+
elif manufacturer == "Adva":
|
|
50
|
+
return "adva8"
|
|
51
|
+
elif manufacturer == "Arista":
|
|
52
|
+
return "eos4"
|
|
53
|
+
raise ValueError(f"unsupported manufacturer {manufacturer}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_supported(manufacturer: str) -> bool:
|
|
57
|
+
if manufacturer not in (
|
|
58
|
+
"Huawei", "Mellanox", "Juniper", "Cisco", "Adva", "Arista",
|
|
59
|
+
):
|
|
60
|
+
logger.warning("Unsupported manufacturer `%s`", manufacturer)
|
|
61
|
+
return False
|
|
62
|
+
return True
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
from annet.annlib.netdev.views.hardware import HardwareView
|
|
6
|
+
from annet.storage import Storage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Entity:
|
|
11
|
+
id: int
|
|
12
|
+
name: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Label:
|
|
17
|
+
value: str
|
|
18
|
+
label: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class IpFamily:
|
|
23
|
+
value: int
|
|
24
|
+
label: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class DeviceType:
|
|
29
|
+
id: int
|
|
30
|
+
manufacturer: Entity
|
|
31
|
+
model: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DeviceIp:
|
|
36
|
+
id: int
|
|
37
|
+
display: str
|
|
38
|
+
address: str
|
|
39
|
+
family: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class IpAddress:
|
|
44
|
+
id: int
|
|
45
|
+
assigned_object_id: int
|
|
46
|
+
display: str
|
|
47
|
+
family: IpFamily
|
|
48
|
+
address: str
|
|
49
|
+
status: Label
|
|
50
|
+
tags: List[Entity]
|
|
51
|
+
created: datetime
|
|
52
|
+
last_updated: datetime
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Interface(Entity):
|
|
57
|
+
device: Entity
|
|
58
|
+
enabled: bool
|
|
59
|
+
display: str = ""
|
|
60
|
+
ip_addresses: List[IpAddress] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class NetboxDevice(Entity):
|
|
65
|
+
url: str
|
|
66
|
+
storage: Storage
|
|
67
|
+
neighbours_ids: List[int]
|
|
68
|
+
|
|
69
|
+
display: str
|
|
70
|
+
device_type: DeviceType
|
|
71
|
+
device_role: Entity
|
|
72
|
+
tenant: Optional[Entity]
|
|
73
|
+
platform: Optional[Entity]
|
|
74
|
+
serial: str
|
|
75
|
+
asset_tag: Optional[str]
|
|
76
|
+
site: Entity
|
|
77
|
+
rack: Optional[Entity]
|
|
78
|
+
position: Optional[float]
|
|
79
|
+
face: Optional[Label]
|
|
80
|
+
status: Label
|
|
81
|
+
primary_ip: Optional[DeviceIp]
|
|
82
|
+
primary_ip4: Optional[DeviceIp]
|
|
83
|
+
primary_ip6: Optional[DeviceIp]
|
|
84
|
+
tags: List[Entity]
|
|
85
|
+
custom_fields: Dict[str, Any]
|
|
86
|
+
created: datetime
|
|
87
|
+
last_updated: datetime
|
|
88
|
+
|
|
89
|
+
fqdn: str
|
|
90
|
+
hostname: str
|
|
91
|
+
hw: Optional[HardwareView]
|
|
92
|
+
breed: str
|
|
93
|
+
|
|
94
|
+
interfaces: List[Interface]
|
|
95
|
+
|
|
96
|
+
# compat
|
|
97
|
+
|
|
98
|
+
def __hash__(self):
|
|
99
|
+
return hash((self.id, type(self)))
|
|
100
|
+
|
|
101
|
+
def __eq__(self, other):
|
|
102
|
+
return type(self) is type(other) and self.url == other.url
|
|
103
|
+
|
|
104
|
+
def is_pc(self):
|
|
105
|
+
return self.device_type.manufacturer.name == "Mellanox"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Union, Iterable, Optional
|
|
3
|
+
|
|
4
|
+
from annet.storage import Query
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class NetboxQuery(Query):
|
|
9
|
+
query: List[str]
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def new(
|
|
13
|
+
cls, query: Union[str, Iterable[str]],
|
|
14
|
+
hosts_range: Optional[slice] = None,
|
|
15
|
+
) -> "NetboxQuery":
|
|
16
|
+
if hosts_range is not None:
|
|
17
|
+
raise ValueError("host_range is not supported")
|
|
18
|
+
return cls(query=list(query))
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def globs(self):
|
|
22
|
+
# We process every query host as a glob
|
|
23
|
+
return self.query
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from adaptix import Retort, name_mapping, NameStyle
|
|
5
|
+
from dataclass_rest import get
|
|
6
|
+
from dataclass_rest.client_protocol import FactoryProtocol
|
|
7
|
+
|
|
8
|
+
from .client import BaseNetboxClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Status:
|
|
13
|
+
netbox_version: str
|
|
14
|
+
plugins: Dict[str, str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NetboxStatusClient(BaseNetboxClient):
|
|
18
|
+
def _init_response_body_factory(self) -> FactoryProtocol:
|
|
19
|
+
return Retort(recipe=[
|
|
20
|
+
name_mapping(name_style=NameStyle.LOWER_KEBAB)
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
@get("status")
|
|
24
|
+
def status(self) -> Status:
|
|
25
|
+
...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NetboxStorageOpts:
|
|
5
|
+
def __init__(self, url: str, token: str):
|
|
6
|
+
self.url = url
|
|
7
|
+
self.token = token
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_cli_opts(cls, cli_opts):
|
|
11
|
+
return cls(
|
|
12
|
+
url=os.getenv("NETBOX_URL", "http://localhost"),
|
|
13
|
+
token=os.getenv("NETBOX_TOKEN", "").strip(),
|
|
14
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclass_rest.exceptions import ClientError
|
|
2
|
+
|
|
3
|
+
from annet.storage import StorageProvider, Storage
|
|
4
|
+
from .common.status_client import NetboxStatusClient
|
|
5
|
+
from .common.storage_opts import NetboxStorageOpts
|
|
6
|
+
from .common.query import NetboxQuery
|
|
7
|
+
from .v24.storage import NetboxStorageV24
|
|
8
|
+
from .v37.storage import NetboxStorageV37
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def storage_factory(opts: NetboxStorageOpts) -> Storage:
|
|
12
|
+
client = NetboxStatusClient(opts.url, opts.token)
|
|
13
|
+
try:
|
|
14
|
+
status = client.status()
|
|
15
|
+
except ClientError as e:
|
|
16
|
+
if e.status_code == 404:
|
|
17
|
+
# old version do not support status reqeust
|
|
18
|
+
return NetboxStorageV24(opts)
|
|
19
|
+
raise
|
|
20
|
+
if status.netbox_version.startswith("3."):
|
|
21
|
+
return NetboxStorageV37(opts)
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError(f"Unsupported version: {status.netbox_version}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NetboxProvider(StorageProvider):
|
|
27
|
+
def storage(self):
|
|
28
|
+
return storage_factory
|
|
29
|
+
|
|
30
|
+
def opts(self):
|
|
31
|
+
return NetboxStorageOpts
|
|
32
|
+
|
|
33
|
+
def query(self):
|
|
34
|
+
return NetboxQuery
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
from annet.adapters.netbox.common.models import Entity, DeviceType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Label:
|
|
10
|
+
value: int
|
|
11
|
+
label: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DeviceIp:
|
|
16
|
+
id: int
|
|
17
|
+
address: str
|
|
18
|
+
family: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Device(Entity):
|
|
23
|
+
url: str
|
|
24
|
+
display_name: str
|
|
25
|
+
device_type: DeviceType
|
|
26
|
+
device_role: Entity
|
|
27
|
+
tenant: Optional[Entity]
|
|
28
|
+
platform: Optional[Entity]
|
|
29
|
+
serial: str
|
|
30
|
+
asset_tag: Optional[str]
|
|
31
|
+
site: Entity
|
|
32
|
+
rack: Optional[Entity]
|
|
33
|
+
position: Optional[float]
|
|
34
|
+
face: Optional[Label]
|
|
35
|
+
status: Label
|
|
36
|
+
primary_ip: Optional[DeviceIp]
|
|
37
|
+
primary_ip4: Optional[DeviceIp]
|
|
38
|
+
primary_ip6: Optional[DeviceIp]
|
|
39
|
+
tags: List[str]
|
|
40
|
+
custom_fields: Dict[str, Any]
|
|
41
|
+
created: datetime
|
|
42
|
+
last_updated: datetime
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Interface(Entity):
|
|
47
|
+
device: Entity
|
|
48
|
+
enabled: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Vrf(Entity):
|
|
53
|
+
rd: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class IpAddress:
|
|
58
|
+
id: int
|
|
59
|
+
family: int
|
|
60
|
+
address: str
|
|
61
|
+
vrf: Optional[Vrf]
|
|
62
|
+
tenant: Any # ???
|
|
63
|
+
status: Label
|
|
64
|
+
description: Optional[str]
|
|
65
|
+
custom_fields: Dict[str, Any]
|
|
66
|
+
tags: List[str]
|
|
67
|
+
created: datetime
|
|
68
|
+
last_updated: datetime
|
|
69
|
+
|
|
70
|
+
interface: Entity
|
|
71
|
+
|
|
72
|
+
nat_inside: Any # ???
|
|
73
|
+
nat_outside: Any # ???
|
|
@@ -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
|