netbox-inventory-toolkit 0.1.6__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.
- netbox_inventory_toolkit/__init__.py +8 -0
- netbox_inventory_toolkit/__main__.py +6 -0
- netbox_inventory_toolkit/cli.py +49 -0
- netbox_inventory_toolkit/collect/__init__.py +1 -0
- netbox_inventory_toolkit/collect/__main__.py +6 -0
- netbox_inventory_toolkit/collect/build.py +488 -0
- netbox_inventory_toolkit/collect/cli.py +236 -0
- netbox_inventory_toolkit/collect/config.py +69 -0
- netbox_inventory_toolkit/collect/context.py +80 -0
- netbox_inventory_toolkit/collect/hardware.py +312 -0
- netbox_inventory_toolkit/collect/identity.py +143 -0
- netbox_inventory_toolkit/collect/log.py +58 -0
- netbox_inventory_toolkit/collect/manual.py +117 -0
- netbox_inventory_toolkit/collect/pipeline.py +205 -0
- netbox_inventory_toolkit/collect/process.py +60 -0
- netbox_inventory_toolkit/collect/render.py +43 -0
- netbox_inventory_toolkit/collect/snmp_merge.py +813 -0
- netbox_inventory_toolkit/collect/sources/__init__.py +1 -0
- netbox_inventory_toolkit/collect/sources/base.py +57 -0
- netbox_inventory_toolkit/collect/sources/ipmi.py +320 -0
- netbox_inventory_toolkit/collect/sources/redfish.py +318 -0
- netbox_inventory_toolkit/collect/sources/snmp.py +394 -0
- netbox_inventory_toolkit/collect/sources/snmp_oids.py +141 -0
- netbox_inventory_toolkit/collect/sources/talos.py +321 -0
- netbox_inventory_toolkit/collect/sources/unifi.py +475 -0
- netbox_inventory_toolkit/inventory/__init__.py +1 -0
- netbox_inventory_toolkit/inventory/choices.py +614 -0
- netbox_inventory_toolkit/inventory/fields.py +710 -0
- netbox_inventory_toolkit/inventory/index.py +2011 -0
- netbox_inventory_toolkit/inventory/keys.py +102 -0
- netbox_inventory_toolkit/inventory/problems.py +76 -0
- netbox_inventory_toolkit/inventory/racks.py +350 -0
- netbox_inventory_toolkit/inventory/schema.py +38 -0
- netbox_inventory_toolkit/inventory/util.py +106 -0
- netbox_inventory_toolkit/lsp.py +375 -0
- netbox_inventory_toolkit/sync/__init__.py +1 -0
- netbox_inventory_toolkit/sync/__main__.py +6 -0
- netbox_inventory_toolkit/sync/api.py +57 -0
- netbox_inventory_toolkit/sync/catalog.py +366 -0
- netbox_inventory_toolkit/sync/cli.py +185 -0
- netbox_inventory_toolkit/sync/context.py +222 -0
- netbox_inventory_toolkit/sync/devices.py +405 -0
- netbox_inventory_toolkit/sync/dtl.py +311 -0
- netbox_inventory_toolkit/sync/images.py +206 -0
- netbox_inventory_toolkit/sync/inventory.py +298 -0
- netbox_inventory_toolkit/sync/meta.py +364 -0
- netbox_inventory_toolkit/sync/network.py +161 -0
- netbox_inventory_toolkit/sync/prune.py +230 -0
- netbox_inventory_toolkit/sync/reconciler.py +46 -0
- netbox_inventory_toolkit/sync/templates.py +89 -0
- netbox_inventory_toolkit/sync/validate.py +902 -0
- netbox_inventory_toolkit-0.1.6.dist-info/METADATA +186 -0
- netbox_inventory_toolkit-0.1.6.dist-info/RECORD +57 -0
- netbox_inventory_toolkit-0.1.6.dist-info/WHEEL +5 -0
- netbox_inventory_toolkit-0.1.6.dist-info/entry_points.txt +2 -0
- netbox_inventory_toolkit-0.1.6.dist-info/licenses/LICENSE +21 -0
- netbox_inventory_toolkit-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""NetBox inventory toolkit."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("netbox-inventory-toolkit")
|
|
7
|
+
except PackageNotFoundError: # running from a source tree without installed metadata
|
|
8
|
+
__version__ = "0.1.6"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Command line entry point for the NetBox inventory toolkit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
|
|
9
|
+
USAGE = """\
|
|
10
|
+
usage: netbox-inventory-toolkit <command> [options]
|
|
11
|
+
|
|
12
|
+
commands:
|
|
13
|
+
collect collect live source data into the generated inventory layer
|
|
14
|
+
sync reconcile the inventory into NetBox
|
|
15
|
+
check validate the inventory without contacting NetBox
|
|
16
|
+
lsp run the inventory language server (stdio)
|
|
17
|
+
version print the package version
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> int:
|
|
22
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
23
|
+
if not args or args[0] in {"-h", "--help", "help"}:
|
|
24
|
+
print(USAGE, end="")
|
|
25
|
+
return 0
|
|
26
|
+
if args[0] in {"--version", "version"}:
|
|
27
|
+
print(f"netbox-inventory-toolkit {__version__}")
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
command, rest = args[0], args[1:]
|
|
31
|
+
if command == "collect":
|
|
32
|
+
from .collect.cli import main as collect_main
|
|
33
|
+
|
|
34
|
+
return collect_main(rest)
|
|
35
|
+
if command in {"sync", "check"}:
|
|
36
|
+
from .sync.cli import main as sync_main
|
|
37
|
+
|
|
38
|
+
return sync_main([*rest, "--check"] if command == "check" else rest)
|
|
39
|
+
if command == "lsp":
|
|
40
|
+
from .lsp import main as lsp_main
|
|
41
|
+
|
|
42
|
+
return lsp_main(rest)
|
|
43
|
+
|
|
44
|
+
print(f"error: unknown command {command!r}\n\n{USAGE}", file=sys.stderr, end="")
|
|
45
|
+
return 2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Inventory collection pipeline."""
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Build canonical generated device entries from source observations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from netbox_inventory_toolkit.inventory.util import normalize_mac
|
|
9
|
+
|
|
10
|
+
from .config import compute_roles, default_tag, require_site, talos_platform, talos_roles
|
|
11
|
+
from .hardware import (
|
|
12
|
+
add_bmc_components,
|
|
13
|
+
add_module_component,
|
|
14
|
+
clean_model,
|
|
15
|
+
disk_bay_type,
|
|
16
|
+
disk_manufacturer,
|
|
17
|
+
infer_memory_manufacturer,
|
|
18
|
+
memory_class,
|
|
19
|
+
normalize_manufacturer,
|
|
20
|
+
register_module_type,
|
|
21
|
+
)
|
|
22
|
+
from .log import get_logger
|
|
23
|
+
from .sources.talos import (
|
|
24
|
+
cpu_cores,
|
|
25
|
+
memory_gib,
|
|
26
|
+
node_bond_members,
|
|
27
|
+
node_disks,
|
|
28
|
+
node_link,
|
|
29
|
+
node_link_macs,
|
|
30
|
+
talos_table,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
log = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _manual_interface_type(device: dict[str, Any] | None, name: str) -> str:
|
|
37
|
+
for interface in (device or {}).get("interfaces") or []:
|
|
38
|
+
if str(interface.get("name") or "") == name and interface.get("type"):
|
|
39
|
+
return str(interface["type"])
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_servers(
|
|
44
|
+
clients: list[dict],
|
|
45
|
+
manual: dict[str, Any],
|
|
46
|
+
switch_name: str = "",
|
|
47
|
+
*,
|
|
48
|
+
module_types: dict[str, dict[str, Any]] | None = None,
|
|
49
|
+
bmc_by_name: dict[str, dict[str, Any]] | None = None,
|
|
50
|
+
) -> list[dict]:
|
|
51
|
+
clients_by_ip = {
|
|
52
|
+
str(client.get("ip")): client
|
|
53
|
+
for client in clients
|
|
54
|
+
if client.get("is_wired") and client.get("ip")
|
|
55
|
+
}
|
|
56
|
+
clients_by_mac = {
|
|
57
|
+
normalize_mac(client["mac"]): client
|
|
58
|
+
for client in clients
|
|
59
|
+
if client.get("is_wired") and client.get("mac")
|
|
60
|
+
}
|
|
61
|
+
site_slug = require_site(manual)
|
|
62
|
+
tag = default_tag(manual)
|
|
63
|
+
servers: list[dict] = []
|
|
64
|
+
catalog = module_types if module_types is not None else {}
|
|
65
|
+
bmc_data = bmc_by_name or {}
|
|
66
|
+
|
|
67
|
+
for device in manual["devices"]:
|
|
68
|
+
if device.get("talos_node") or str(device.get("role") or "") not in compute_roles(manual):
|
|
69
|
+
continue
|
|
70
|
+
ip = str(device.get("unifi_ip") or "")
|
|
71
|
+
claimed_mac = str(device.get("unifi_mac") or "")
|
|
72
|
+
client = clients_by_ip.get(ip) if ip else None
|
|
73
|
+
if client is None and claimed_mac:
|
|
74
|
+
client = clients_by_mac.get(normalize_mac(claimed_mac))
|
|
75
|
+
if not ip and not client:
|
|
76
|
+
continue
|
|
77
|
+
if not ip and client:
|
|
78
|
+
ip = str(client.get("ip") or "")
|
|
79
|
+
if not ip:
|
|
80
|
+
continue
|
|
81
|
+
name = str(device.get("name") or "")
|
|
82
|
+
uplinks = device.get("uplinks") or []
|
|
83
|
+
port = (client or {}).get("sw_port")
|
|
84
|
+
mac = str((client or {}).get("mac") or "")
|
|
85
|
+
unifi_name = str((client or {}).get("name") or (client or {}).get("hostname") or "")
|
|
86
|
+
status = device.get("status") or ("active" if client else "offline")
|
|
87
|
+
device_type = str(device.get("device_type") or "")
|
|
88
|
+
|
|
89
|
+
# The management address belongs on the physical NIC that carries the
|
|
90
|
+
# traffic to the switch, not on a virtual "mgmt" interface.
|
|
91
|
+
if uplinks:
|
|
92
|
+
primary_iface = str(uplinks[0].get("interface") or "Port 1")
|
|
93
|
+
else:
|
|
94
|
+
primary_iface = "Port 1"
|
|
95
|
+
|
|
96
|
+
interfaces: list[dict] = []
|
|
97
|
+
interface_sources = ["manual", "unifi"] if client else ["manual"]
|
|
98
|
+
if uplinks:
|
|
99
|
+
for uplink in uplinks:
|
|
100
|
+
iface_name = str(uplink.get("interface") or "Port 1")
|
|
101
|
+
entry: dict[str, Any] = {
|
|
102
|
+
"name": iface_name,
|
|
103
|
+
"type": "1000base-t",
|
|
104
|
+
"enabled": True,
|
|
105
|
+
"description": f"Uplink to {uplink.get('switch')} Port {uplink.get('port')}",
|
|
106
|
+
"sources": interface_sources,
|
|
107
|
+
}
|
|
108
|
+
if iface_name == primary_iface:
|
|
109
|
+
if mac:
|
|
110
|
+
entry["mac_address"] = mac
|
|
111
|
+
entry["ip_addresses"] = [
|
|
112
|
+
{"address": f"{ip}/24", "dns_name": name, "primary": True}
|
|
113
|
+
]
|
|
114
|
+
interfaces.append(entry)
|
|
115
|
+
else:
|
|
116
|
+
interfaces.append(
|
|
117
|
+
{
|
|
118
|
+
"name": "Port 1",
|
|
119
|
+
"type": "1000base-t",
|
|
120
|
+
"enabled": True,
|
|
121
|
+
"description": "Server network uplink",
|
|
122
|
+
"mac_address": mac or None,
|
|
123
|
+
"ip_addresses": [{"address": f"{ip}/24", "dns_name": name, "primary": True}],
|
|
124
|
+
"sources": interface_sources,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
description = f"{name} at {ip}"
|
|
129
|
+
if unifi_name:
|
|
130
|
+
description += f" (UniFi client {unifi_name})"
|
|
131
|
+
if port:
|
|
132
|
+
link = f"{switch_name} Port {port}" if switch_name else f"Port {port}"
|
|
133
|
+
description += f", {link}"
|
|
134
|
+
if mac:
|
|
135
|
+
description += f", MAC {mac}"
|
|
136
|
+
if client is None:
|
|
137
|
+
description += "; no active UniFi client"
|
|
138
|
+
|
|
139
|
+
module_bays: list[dict] = []
|
|
140
|
+
modules: list[dict] = []
|
|
141
|
+
bmc = bmc_data.get(name)
|
|
142
|
+
sources = set(interface_sources)
|
|
143
|
+
if bmc:
|
|
144
|
+
sources.add(str(bmc.get("source") or "redfish"))
|
|
145
|
+
add_bmc_components(module_bays, modules, catalog, bmc, device_type)
|
|
146
|
+
|
|
147
|
+
server: dict[str, Any] = {
|
|
148
|
+
"name": name,
|
|
149
|
+
"site": str(device.get("site") or site_slug),
|
|
150
|
+
"role": str(device.get("role") or "server"),
|
|
151
|
+
"device_type": device_type,
|
|
152
|
+
"status": status,
|
|
153
|
+
"serial": str(device.get("serial") or ""),
|
|
154
|
+
"description": description,
|
|
155
|
+
"tags": sorted({tag, *[str(item) for item in device.get("tags") or []]}),
|
|
156
|
+
"sources": sorted(sources),
|
|
157
|
+
"interfaces": interfaces,
|
|
158
|
+
}
|
|
159
|
+
if module_bays:
|
|
160
|
+
server["module_bays"] = module_bays
|
|
161
|
+
if modules:
|
|
162
|
+
server["modules"] = modules
|
|
163
|
+
log.debug(
|
|
164
|
+
"Built server %s: interfaces=%d, module_bays=%d, modules=%d",
|
|
165
|
+
name,
|
|
166
|
+
len(interfaces),
|
|
167
|
+
len(module_bays),
|
|
168
|
+
len(modules),
|
|
169
|
+
)
|
|
170
|
+
servers.append(server)
|
|
171
|
+
|
|
172
|
+
# Devices that are not UniFi clients or Talos nodes, but do have BMC
|
|
173
|
+
# observations. They already exist in the manual layer; this sparse entry
|
|
174
|
+
# only attaches the discovered components.
|
|
175
|
+
for device in manual["devices"]:
|
|
176
|
+
if (
|
|
177
|
+
device.get("talos_node")
|
|
178
|
+
or device.get("unifi_ip")
|
|
179
|
+
or str(device.get("role") or "") not in compute_roles(manual)
|
|
180
|
+
):
|
|
181
|
+
continue
|
|
182
|
+
name = str(device.get("name") or "")
|
|
183
|
+
bmc = bmc_data.get(name)
|
|
184
|
+
if not name or not bmc:
|
|
185
|
+
continue
|
|
186
|
+
module_bays: list[dict] = []
|
|
187
|
+
modules: list[dict] = []
|
|
188
|
+
add_bmc_components(
|
|
189
|
+
module_bays, modules, catalog, bmc, str(device.get("device_type") or "")
|
|
190
|
+
)
|
|
191
|
+
if not modules:
|
|
192
|
+
continue
|
|
193
|
+
log.debug(
|
|
194
|
+
"Built BMC-only server %s: module_bays=%d, modules=%d",
|
|
195
|
+
name,
|
|
196
|
+
len(module_bays),
|
|
197
|
+
len(modules),
|
|
198
|
+
)
|
|
199
|
+
servers.append(
|
|
200
|
+
{
|
|
201
|
+
"name": name,
|
|
202
|
+
"site": str(device.get("site") or site_slug),
|
|
203
|
+
"role": str(device.get("role") or "server"),
|
|
204
|
+
"device_type": str(device.get("device_type") or ""),
|
|
205
|
+
"status": str(device.get("status") or "active"),
|
|
206
|
+
"serial": str(device.get("serial") or ""),
|
|
207
|
+
"description": str(device.get("description") or f"{name} via BMC"),
|
|
208
|
+
"tags": sorted(
|
|
209
|
+
{tag, *[str(item) for item in device.get("tags") or []]}
|
|
210
|
+
),
|
|
211
|
+
"sources": sorted({"manual", str(bmc.get("source") or "redfish")}),
|
|
212
|
+
"module_bays": module_bays,
|
|
213
|
+
"modules": modules,
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
return servers
|
|
217
|
+
|
|
218
|
+
def build_nodes(
|
|
219
|
+
nodes: list[dict],
|
|
220
|
+
talos_dir: Path,
|
|
221
|
+
manual: dict[str, Any],
|
|
222
|
+
*,
|
|
223
|
+
talosctl: str | None = None,
|
|
224
|
+
talosconfig: str | None = None,
|
|
225
|
+
module_types: dict[str, dict[str, Any]] | None = None,
|
|
226
|
+
bmc_by_name: dict[str, dict[str, Any]] | None = None,
|
|
227
|
+
manual_by_node: dict[str, dict[str, Any]] | None = None,
|
|
228
|
+
) -> list[dict]:
|
|
229
|
+
result: list[dict] = []
|
|
230
|
+
site_slug = require_site(manual)
|
|
231
|
+
tag = default_tag(manual)
|
|
232
|
+
manual_devices = manual["devices"]
|
|
233
|
+
manual_nodes = manual_by_node or {}
|
|
234
|
+
catalog = module_types if module_types is not None else {}
|
|
235
|
+
bmc_data = bmc_by_name or {}
|
|
236
|
+
|
|
237
|
+
for node in nodes:
|
|
238
|
+
metadata = node.get("metadata") or {}
|
|
239
|
+
status = node.get("status") or {}
|
|
240
|
+
node_info = status.get("nodeInfo") or {}
|
|
241
|
+
capacity = status.get("capacity") or {}
|
|
242
|
+
labels = metadata.get("labels") or {}
|
|
243
|
+
name = metadata.get("name")
|
|
244
|
+
if not name:
|
|
245
|
+
continue
|
|
246
|
+
manual_device = manual_nodes.get(name)
|
|
247
|
+
if manual_device is None:
|
|
248
|
+
manual_device = next(
|
|
249
|
+
(
|
|
250
|
+
device
|
|
251
|
+
for device in manual_devices
|
|
252
|
+
if device.get("talos_node") == name or device.get("name") == name
|
|
253
|
+
),
|
|
254
|
+
None,
|
|
255
|
+
)
|
|
256
|
+
if manual_device is None:
|
|
257
|
+
log.warning("Talos node %s has no manual device entry", name)
|
|
258
|
+
device_name = str((manual_device or {}).get("name") or name)
|
|
259
|
+
|
|
260
|
+
internal_ip = None
|
|
261
|
+
for address in status.get("addresses") or []:
|
|
262
|
+
if address.get("type") == "InternalIP":
|
|
263
|
+
internal_ip = address.get("address")
|
|
264
|
+
break
|
|
265
|
+
internal_endpoint = str(internal_ip) if internal_ip else None
|
|
266
|
+
link = node_link(name, talos_dir)
|
|
267
|
+
if link:
|
|
268
|
+
iface_name, iface_address = link
|
|
269
|
+
else:
|
|
270
|
+
iface_name = "eth0"
|
|
271
|
+
iface_address = f"{internal_ip}/24" if internal_ip else None
|
|
272
|
+
if not iface_address:
|
|
273
|
+
continue
|
|
274
|
+
ready = any(
|
|
275
|
+
condition.get("type") == "Ready" and condition.get("status") == "True"
|
|
276
|
+
for condition in status.get("conditions") or []
|
|
277
|
+
)
|
|
278
|
+
control_plane_role, worker_role = talos_roles(manual)
|
|
279
|
+
role = control_plane_role if "node-role.kubernetes.io/control-plane" in labels else worker_role
|
|
280
|
+
if manual_device and manual_device.get("role"):
|
|
281
|
+
role = str(manual_device["role"])
|
|
282
|
+
device_type = str((manual_device or {}).get("device_type") or "")
|
|
283
|
+
description = (
|
|
284
|
+
f"{node_info.get('osImage', 'Talos')}, Kubernetes "
|
|
285
|
+
f"{node_info.get('kubeletVersion', 'unknown')}, kernel "
|
|
286
|
+
f"{node_info.get('kernelVersion', 'unknown')}, "
|
|
287
|
+
f"{cpu_cores(capacity.get('cpu')):g} CPU, "
|
|
288
|
+
f"{memory_gib(capacity.get('memory')):.1f} GiB RAM"
|
|
289
|
+
)
|
|
290
|
+
interfaces: list[dict] = []
|
|
291
|
+
members = node_bond_members(name, talos_dir)
|
|
292
|
+
macs: dict[str, str] = {}
|
|
293
|
+
if talosctl:
|
|
294
|
+
macs = node_link_macs(
|
|
295
|
+
talosctl, talosconfig, name, endpoint=internal_endpoint
|
|
296
|
+
)
|
|
297
|
+
for member in members:
|
|
298
|
+
entry: dict[str, Any] = {
|
|
299
|
+
"name": member,
|
|
300
|
+
"type": "1000base-t",
|
|
301
|
+
"enabled": True,
|
|
302
|
+
"description": "bond0 member (LACP)",
|
|
303
|
+
"sources": ["talos"],
|
|
304
|
+
}
|
|
305
|
+
if member in macs:
|
|
306
|
+
entry["mac_address"] = macs[member]
|
|
307
|
+
interfaces.append(entry)
|
|
308
|
+
if members:
|
|
309
|
+
iface_type = "lag"
|
|
310
|
+
else:
|
|
311
|
+
iface_type = _manual_interface_type(manual_device, iface_name) or "1000base-t"
|
|
312
|
+
primary_entry: dict[str, Any] = {
|
|
313
|
+
"name": iface_name,
|
|
314
|
+
"type": iface_type,
|
|
315
|
+
"enabled": True,
|
|
316
|
+
"description": "In-band management and cluster traffic",
|
|
317
|
+
"sources": ["talos"],
|
|
318
|
+
"ip_addresses": [
|
|
319
|
+
{
|
|
320
|
+
"address": iface_address,
|
|
321
|
+
"dns_name": name,
|
|
322
|
+
"primary": True,
|
|
323
|
+
}
|
|
324
|
+
],
|
|
325
|
+
}
|
|
326
|
+
if iface_name in macs:
|
|
327
|
+
primary_entry["mac_address"] = macs[iface_name]
|
|
328
|
+
interfaces.append(primary_entry)
|
|
329
|
+
|
|
330
|
+
module_bays: list[dict] = []
|
|
331
|
+
modules: list[dict] = []
|
|
332
|
+
bay_disk_type = disk_bay_type(device_type)
|
|
333
|
+
dimm_class = memory_class(device_type)
|
|
334
|
+
bmc = bmc_data.get(device_name) or bmc_data.get(name)
|
|
335
|
+
|
|
336
|
+
if bmc:
|
|
337
|
+
add_bmc_components(module_bays, modules, catalog, bmc, device_type)
|
|
338
|
+
|
|
339
|
+
if not bmc or not bmc.get("cpu"):
|
|
340
|
+
if talosctl:
|
|
341
|
+
for index, processor in enumerate(
|
|
342
|
+
talos_table(
|
|
343
|
+
talosctl, talosconfig, name, "processors",
|
|
344
|
+
endpoint=internal_endpoint,
|
|
345
|
+
), 1
|
|
346
|
+
):
|
|
347
|
+
spec = processor.get("spec") or {}
|
|
348
|
+
if not spec.get("productName") and not spec.get("coreCount"):
|
|
349
|
+
continue
|
|
350
|
+
model = clean_model(str(spec.get("productName") or f"CPU {index}"))
|
|
351
|
+
manufacturer = normalize_manufacturer(
|
|
352
|
+
str(spec.get("manufacturer") or "Intel")
|
|
353
|
+
)
|
|
354
|
+
module_type = register_module_type(
|
|
355
|
+
catalog,
|
|
356
|
+
manufacturer=manufacturer,
|
|
357
|
+
model=model,
|
|
358
|
+
profile="CPU",
|
|
359
|
+
attributes={
|
|
360
|
+
"cores": int(spec.get("coreCount") or 0),
|
|
361
|
+
"speed": float(
|
|
362
|
+
spec.get("bootSpeedMhz") or spec.get("maxSpeedMhz") or 0
|
|
363
|
+
)
|
|
364
|
+
/ 1000,
|
|
365
|
+
"architecture": "x86_64",
|
|
366
|
+
},
|
|
367
|
+
bay_types=["cpu-socket"],
|
|
368
|
+
part_number=model,
|
|
369
|
+
description="Processor",
|
|
370
|
+
sources=["talos"],
|
|
371
|
+
)
|
|
372
|
+
add_module_component(
|
|
373
|
+
module_bays,
|
|
374
|
+
modules,
|
|
375
|
+
str(spec.get("socket") or f"CPU {index}"),
|
|
376
|
+
"cpu-socket",
|
|
377
|
+
"Processor socket.",
|
|
378
|
+
module_type,
|
|
379
|
+
description=model,
|
|
380
|
+
sources=["talos"],
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if not bmc or not bmc.get("memory"):
|
|
384
|
+
if talosctl:
|
|
385
|
+
for index, memory in enumerate(
|
|
386
|
+
talos_table(
|
|
387
|
+
talosctl, talosconfig, name, "memorymodules",
|
|
388
|
+
endpoint=internal_endpoint,
|
|
389
|
+
), 1
|
|
390
|
+
):
|
|
391
|
+
spec = memory.get("spec") or {}
|
|
392
|
+
if not spec.get("sizeMiB"):
|
|
393
|
+
continue
|
|
394
|
+
locator = str(spec.get("deviceLocator") or f"DIMM {index}")
|
|
395
|
+
bay_name = locator if locator.upper().startswith("DIMM") else f"DIMM {locator}"
|
|
396
|
+
model = str(spec.get("productName") or locator)
|
|
397
|
+
size_gb = int(spec.get("sizeMiB") or 0) // 1024
|
|
398
|
+
data_rate = int(spec.get("speed") or 0)
|
|
399
|
+
mem_manufacturer = infer_memory_manufacturer(
|
|
400
|
+
model, normalize_manufacturer(str(spec.get("manufacturer") or ""))
|
|
401
|
+
)
|
|
402
|
+
module_type = register_module_type(
|
|
403
|
+
catalog,
|
|
404
|
+
manufacturer=mem_manufacturer,
|
|
405
|
+
model=model,
|
|
406
|
+
profile="Memory",
|
|
407
|
+
attributes={
|
|
408
|
+
"size": size_gb,
|
|
409
|
+
"class": dimm_class,
|
|
410
|
+
"ecc": True,
|
|
411
|
+
"data_rate": data_rate,
|
|
412
|
+
},
|
|
413
|
+
bay_types=[f"dimm-{dimm_class.lower()}"],
|
|
414
|
+
part_number=model,
|
|
415
|
+
description="Memory module",
|
|
416
|
+
sources=["talos"],
|
|
417
|
+
)
|
|
418
|
+
add_module_component(
|
|
419
|
+
module_bays,
|
|
420
|
+
modules,
|
|
421
|
+
bay_name,
|
|
422
|
+
f"dimm-{dimm_class.lower()}",
|
|
423
|
+
"Memory module slot.",
|
|
424
|
+
module_type,
|
|
425
|
+
serial=str(spec.get("serialNumber") or ""),
|
|
426
|
+
description=f"{size_gb} GB {dimm_class} {data_rate} MT/s",
|
|
427
|
+
sources=["talos"],
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if not bmc or not bmc.get("disks"):
|
|
431
|
+
if talosctl:
|
|
432
|
+
disks, _system_disk = node_disks(talosctl, talosconfig, name, endpoint=internal_endpoint)
|
|
433
|
+
for index, disk in enumerate(disks, 1):
|
|
434
|
+
size_gb = int(disk["size"] / 1_000_000_000)
|
|
435
|
+
kind = "HD" if disk["rotational"] else "SSD"
|
|
436
|
+
model = disk["model"] or f"Disk {index}"
|
|
437
|
+
module_type = register_module_type(
|
|
438
|
+
catalog,
|
|
439
|
+
manufacturer=disk_manufacturer(model),
|
|
440
|
+
model=model,
|
|
441
|
+
profile="Hard disk",
|
|
442
|
+
attributes={"size": size_gb, "type": kind},
|
|
443
|
+
bay_types=[bay_disk_type],
|
|
444
|
+
part_number=model,
|
|
445
|
+
description="Disk drive",
|
|
446
|
+
sources=["talos"],
|
|
447
|
+
)
|
|
448
|
+
add_module_component(
|
|
449
|
+
module_bays,
|
|
450
|
+
modules,
|
|
451
|
+
f"Drive {index}",
|
|
452
|
+
bay_disk_type,
|
|
453
|
+
"Drive bay.",
|
|
454
|
+
module_type,
|
|
455
|
+
serial=disk["serial"],
|
|
456
|
+
description=f"{size_gb} GB {kind}",
|
|
457
|
+
sources=["talos"],
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
device_entry: dict[str, Any] = {
|
|
461
|
+
"name": device_name,
|
|
462
|
+
"site": str((manual_device or {}).get("site") or site_slug),
|
|
463
|
+
"role": role,
|
|
464
|
+
"device_type": device_type,
|
|
465
|
+
|
|
466
|
+
"status": "active" if ready else "offline",
|
|
467
|
+
"serial": node_info.get("systemUUID") or "",
|
|
468
|
+
"description": description,
|
|
469
|
+
"tags": sorted({tag, *[str(item) for item in (manual_device or {}).get("tags") or []]}),
|
|
470
|
+
"sources": sorted({"talos", *([str(bmc.get("source") or "redfish")] if bmc else [])}),
|
|
471
|
+
"interfaces": interfaces,
|
|
472
|
+
}
|
|
473
|
+
platform = talos_platform(manual)
|
|
474
|
+
if platform:
|
|
475
|
+
device_entry["platform"] = platform
|
|
476
|
+
if module_bays:
|
|
477
|
+
device_entry["module_bays"] = module_bays
|
|
478
|
+
if modules:
|
|
479
|
+
device_entry["modules"] = modules
|
|
480
|
+
log.debug(
|
|
481
|
+
"Built Talos node %s: interfaces=%d, module_bays=%d, modules=%d",
|
|
482
|
+
device_name,
|
|
483
|
+
len(interfaces),
|
|
484
|
+
len(module_bays),
|
|
485
|
+
len(modules),
|
|
486
|
+
)
|
|
487
|
+
result.append(device_entry)
|
|
488
|
+
return result
|