annet 0.6__tar.gz → 0.7__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.

Files changed (139) hide show
  1. {annet-0.6/annet.egg-info → annet-0.7}/PKG-INFO +3 -1
  2. {annet-0.6 → annet-0.7}/README.md +9 -0
  3. annet-0.7/annet/adapters/netbox/common/client.py +87 -0
  4. annet-0.7/annet/adapters/netbox/common/manufacturer.py +62 -0
  5. annet-0.7/annet/adapters/netbox/common/models.py +98 -0
  6. annet-0.7/annet/adapters/netbox/common/query.py +23 -0
  7. annet-0.7/annet/adapters/netbox/common/status_client.py +24 -0
  8. annet-0.7/annet/adapters/netbox/common/storage_opts.py +14 -0
  9. annet-0.7/annet/adapters/netbox/provider.py +34 -0
  10. annet-0.7/annet/adapters/netbox/v24/api_models.py +72 -0
  11. annet-0.7/annet/adapters/netbox/v24/client.py +59 -0
  12. annet-0.7/annet/adapters/netbox/v24/storage.py +190 -0
  13. annet-0.7/annet/adapters/netbox/v37/api_models.py +37 -0
  14. annet-0.7/annet/adapters/netbox/v37/client.py +62 -0
  15. annet-0.7/annet/adapters/netbox/v37/storage.py +143 -0
  16. {annet-0.6 → annet-0.7}/annet/api/__init__.py +18 -6
  17. {annet-0.6 → annet-0.7}/annet/cli.py +6 -2
  18. {annet-0.6 → annet-0.7}/annet/cli_args.py +10 -0
  19. {annet-0.6 → annet-0.7}/annet/diff.py +1 -2
  20. {annet-0.6 → annet-0.7}/annet/gen.py +6 -2
  21. {annet-0.6 → annet-0.7}/annet/generators/__init__.py +7 -9
  22. {annet-0.6 → annet-0.7}/annet/output.py +3 -1
  23. annet-0.7/annet/rulebook/arista/__init__.py +0 -0
  24. annet-0.7/annet/rulebook/cisco/__init__.py +0 -0
  25. annet-0.7/annet/rulebook/huawei/__init__.py +0 -0
  26. annet-0.7/annet/rulebook/nexus/__init__.py +0 -0
  27. {annet-0.6 → annet-0.7/annet.egg-info}/PKG-INFO +3 -1
  28. {annet-0.6 → annet-0.7}/annet.egg-info/SOURCES.txt +19 -3
  29. annet-0.7/annet.egg-info/entry_points.txt +5 -0
  30. {annet-0.6 → annet-0.7}/annet.egg-info/requires.txt +2 -0
  31. {annet-0.6 → annet-0.7}/annet.egg-info/top_level.txt +0 -1
  32. annet-0.7/annet_generators/__init__.py +0 -0
  33. {annet-0.6 → annet-0.7}/requirements.txt +2 -0
  34. {annet-0.6 → annet-0.7}/setup.py +1 -4
  35. annet-0.6/annet.egg-info/entry_points.txt +0 -6
  36. annet-0.6/annet_nbexport/__init__.py +0 -220
  37. annet-0.6/annet_nbexport/main.py +0 -46
  38. {annet-0.6 → annet-0.7}/AUTHORS +0 -0
  39. {annet-0.6 → annet-0.7}/LICENSE +0 -0
  40. {annet-0.6 → annet-0.7}/MANIFEST.in +0 -0
  41. {annet-0.6 → annet-0.7}/annet/__init__.py +0 -0
  42. {annet-0.6/annet/annlib/netdev → annet-0.7/annet/adapters}/__init__.py +0 -0
  43. {annet-0.6/annet/annlib/netdev/views → annet-0.7/annet/adapters/netbox}/__init__.py +0 -0
  44. {annet-0.6/annet/annlib/rbparser → annet-0.7/annet/adapters/netbox/common}/__init__.py +0 -0
  45. {annet-0.6/annet/annlib/rulebook → annet-0.7/annet/adapters/netbox/v24}/__init__.py +0 -0
  46. {annet-0.6/annet/generators/common → annet-0.7/annet/adapters/netbox/v37}/__init__.py +0 -0
  47. {annet-0.6 → annet-0.7}/annet/annet.py +0 -0
  48. {annet-0.6 → annet-0.7}/annet/annlib/__init__.py +0 -0
  49. {annet-0.6 → annet-0.7}/annet/annlib/command.py +0 -0
  50. {annet-0.6 → annet-0.7}/annet/annlib/diff.py +0 -0
  51. {annet-0.6 → annet-0.7}/annet/annlib/errors.py +0 -0
  52. {annet-0.6 → annet-0.7}/annet/annlib/filter_acl.py +0 -0
  53. {annet-0.6 → annet-0.7}/annet/annlib/jsontools.py +0 -0
  54. {annet-0.6 → annet-0.7}/annet/annlib/lib.py +0 -0
  55. {annet-0.6/annet/rulebook/arista → annet-0.7/annet/annlib/netdev}/__init__.py +0 -0
  56. {annet-0.6 → annet-0.7}/annet/annlib/netdev/db.py +0 -0
  57. {annet-0.6 → annet-0.7}/annet/annlib/netdev/devdb/__init__.py +0 -0
  58. {annet-0.6 → annet-0.7}/annet/annlib/netdev/devdb/data/devdb.json +0 -0
  59. {annet-0.6/annet/rulebook/cisco → annet-0.7/annet/annlib/netdev/views}/__init__.py +0 -0
  60. {annet-0.6 → annet-0.7}/annet/annlib/netdev/views/dump.py +0 -0
  61. {annet-0.6 → annet-0.7}/annet/annlib/netdev/views/hardware.py +0 -0
  62. {annet-0.6 → annet-0.7}/annet/annlib/output.py +0 -0
  63. {annet-0.6 → annet-0.7}/annet/annlib/patching.py +0 -0
  64. {annet-0.6/annet/rulebook/huawei → annet-0.7/annet/annlib/rbparser}/__init__.py +0 -0
  65. {annet-0.6 → annet-0.7}/annet/annlib/rbparser/acl.py +0 -0
  66. {annet-0.6 → annet-0.7}/annet/annlib/rbparser/deploying.py +0 -0
  67. {annet-0.6 → annet-0.7}/annet/annlib/rbparser/ordering.py +0 -0
  68. {annet-0.6 → annet-0.7}/annet/annlib/rbparser/platform.py +0 -0
  69. {annet-0.6 → annet-0.7}/annet/annlib/rbparser/syntax.py +0 -0
  70. {annet-0.6/annet/rulebook/nexus → annet-0.7/annet/annlib/rulebook}/__init__.py +0 -0
  71. {annet-0.6 → annet-0.7}/annet/annlib/rulebook/common.py +0 -0
  72. {annet-0.6 → annet-0.7}/annet/annlib/tabparser.py +0 -0
  73. {annet-0.6 → annet-0.7}/annet/annlib/types.py +0 -0
  74. {annet-0.6 → annet-0.7}/annet/argparse.py +0 -0
  75. {annet-0.6 → annet-0.7}/annet/configs/context.yml +0 -0
  76. {annet-0.6 → annet-0.7}/annet/configs/logging.yaml +0 -0
  77. {annet-0.6 → annet-0.7}/annet/connectors.py +0 -0
  78. {annet-0.6 → annet-0.7}/annet/deploy.py +0 -0
  79. {annet-0.6 → annet-0.7}/annet/executor.py +0 -0
  80. {annet-0.6 → annet-0.7}/annet/filtering.py +0 -0
  81. {annet-0.6/annet_generators → annet-0.7/annet/generators/common}/__init__.py +0 -0
  82. {annet-0.6 → annet-0.7}/annet/generators/common/initial.py +0 -0
  83. {annet-0.6 → annet-0.7}/annet/hardware.py +0 -0
  84. {annet-0.6 → annet-0.7}/annet/implicit.py +0 -0
  85. {annet-0.6 → annet-0.7}/annet/lib.py +0 -0
  86. {annet-0.6 → annet-0.7}/annet/parallel.py +0 -0
  87. {annet-0.6 → annet-0.7}/annet/patching.py +0 -0
  88. {annet-0.6 → annet-0.7}/annet/reference.py +0 -0
  89. {annet-0.6 → annet-0.7}/annet/rulebook/__init__.py +0 -0
  90. {annet-0.6 → annet-0.7}/annet/rulebook/arista/iface.py +0 -0
  91. {annet-0.6 → annet-0.7}/annet/rulebook/aruba/__init__.py +0 -0
  92. {annet-0.6 → annet-0.7}/annet/rulebook/aruba/ap_env.py +0 -0
  93. {annet-0.6 → annet-0.7}/annet/rulebook/aruba/misc.py +0 -0
  94. {annet-0.6 → annet-0.7}/annet/rulebook/cisco/iface.py +0 -0
  95. {annet-0.6 → annet-0.7}/annet/rulebook/cisco/misc.py +0 -0
  96. {annet-0.6 → annet-0.7}/annet/rulebook/cisco/vlandb.py +0 -0
  97. {annet-0.6 → annet-0.7}/annet/rulebook/common.py +0 -0
  98. {annet-0.6 → annet-0.7}/annet/rulebook/deploying.py +0 -0
  99. {annet-0.6 → annet-0.7}/annet/rulebook/huawei/aaa.py +0 -0
  100. {annet-0.6 → annet-0.7}/annet/rulebook/huawei/bgp.py +0 -0
  101. {annet-0.6 → annet-0.7}/annet/rulebook/huawei/iface.py +0 -0
  102. {annet-0.6 → annet-0.7}/annet/rulebook/huawei/misc.py +0 -0
  103. {annet-0.6 → annet-0.7}/annet/rulebook/huawei/vlandb.py +0 -0
  104. {annet-0.6 → annet-0.7}/annet/rulebook/juniper/__init__.py +0 -0
  105. {annet-0.6 → annet-0.7}/annet/rulebook/nexus/iface.py +0 -0
  106. {annet-0.6 → annet-0.7}/annet/rulebook/patching.py +0 -0
  107. {annet-0.6 → annet-0.7}/annet/rulebook/ribbon/__init__.py +0 -0
  108. {annet-0.6 → annet-0.7}/annet/rulebook/texts/arista.deploy +0 -0
  109. {annet-0.6 → annet-0.7}/annet/rulebook/texts/arista.order +0 -0
  110. {annet-0.6 → annet-0.7}/annet/rulebook/texts/arista.rul +0 -0
  111. {annet-0.6 → annet-0.7}/annet/rulebook/texts/aruba.deploy +0 -0
  112. {annet-0.6 → annet-0.7}/annet/rulebook/texts/aruba.order +0 -0
  113. {annet-0.6 → annet-0.7}/annet/rulebook/texts/aruba.rul +0 -0
  114. {annet-0.6 → annet-0.7}/annet/rulebook/texts/cisco.deploy +0 -0
  115. {annet-0.6 → annet-0.7}/annet/rulebook/texts/cisco.order +0 -0
  116. {annet-0.6 → annet-0.7}/annet/rulebook/texts/cisco.rul +0 -0
  117. {annet-0.6 → annet-0.7}/annet/rulebook/texts/huawei.deploy +0 -0
  118. {annet-0.6 → annet-0.7}/annet/rulebook/texts/huawei.order +0 -0
  119. {annet-0.6 → annet-0.7}/annet/rulebook/texts/huawei.rul +0 -0
  120. {annet-0.6 → annet-0.7}/annet/rulebook/texts/juniper.rul +0 -0
  121. {annet-0.6 → annet-0.7}/annet/rulebook/texts/nexus.deploy +0 -0
  122. {annet-0.6 → annet-0.7}/annet/rulebook/texts/nexus.order +0 -0
  123. {annet-0.6 → annet-0.7}/annet/rulebook/texts/nexus.rul +0 -0
  124. {annet-0.6 → annet-0.7}/annet/rulebook/texts/nokia.rul +0 -0
  125. {annet-0.6 → annet-0.7}/annet/rulebook/texts/pc.order +0 -0
  126. {annet-0.6 → annet-0.7}/annet/rulebook/texts/pc.rul +0 -0
  127. {annet-0.6 → annet-0.7}/annet/rulebook/texts/ribbon.deploy +0 -0
  128. {annet-0.6 → annet-0.7}/annet/rulebook/texts/ribbon.rul +0 -0
  129. {annet-0.6 → annet-0.7}/annet/rulebook/texts/routeros.order +0 -0
  130. {annet-0.6 → annet-0.7}/annet/rulebook/texts/routeros.rul +0 -0
  131. {annet-0.6 → annet-0.7}/annet/storage.py +0 -0
  132. {annet-0.6 → annet-0.7}/annet/tabparser.py +0 -0
  133. {annet-0.6 → annet-0.7}/annet/text_term_format.py +0 -0
  134. {annet-0.6 → annet-0.7}/annet/tracing.py +0 -0
  135. {annet-0.6 → annet-0.7}/annet/types.py +0 -0
  136. {annet-0.6 → annet-0.7}/annet.egg-info/dependency_links.txt +0 -0
  137. {annet-0.6 → annet-0.7}/annet_generators/example/__init__.py +0 -0
  138. {annet-0.6 → annet-0.7}/annet_generators/example/lldp.py +0 -0
  139. {annet-0.6 → annet-0.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: annet
3
- Version: 0.6
3
+ Version: 0.7
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