Skip to content

Commit d472b67

Browse files
authored
Add device role from NetBox to baremetal nodes view (#2079)
Fetch the device role from NetBox (by name or inventory_hostname custom field) in the API backend and display it in the frontend nodes overview as a purple badge and in the node detail page as a dedicated row. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
1 parent c9360c3 commit d472b67

5 files changed

Lines changed: 46 additions & 8 deletions

File tree

frontend/app/nodes/[uuid]/page.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ export default function NodeDetailPage({ params }: { params: Promise<{ uuid: str
9999
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{node.uuid}</dd>
100100
</div>
101101
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
102+
<dt className="text-sm font-medium text-gray-500">Device Role</dt>
103+
<dd className="mt-1 sm:mt-0 sm:col-span-2">
104+
{node.device_role ? (
105+
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">
106+
{node.device_role}
107+
</span>
108+
) : (
109+
<span className="text-sm text-gray-500">-</span>
110+
)}
111+
</dd>
112+
</div>
113+
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
102114
<dt className="text-sm font-medium text-gray-500">Power State</dt>
103115
<dd className="mt-1 sm:mt-0 sm:col-span-2">
104116
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
@@ -110,7 +122,7 @@ export default function NodeDetailPage({ params }: { params: Promise<{ uuid: str
110122
</span>
111123
</dd>
112124
</div>
113-
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
125+
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
114126
<dt className="text-sm font-medium text-gray-500">Provision State</dt>
115127
<dd className="mt-1 sm:mt-0 sm:col-span-2">
116128
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
@@ -128,7 +140,7 @@ export default function NodeDetailPage({ params }: { params: Promise<{ uuid: str
128140
</span>
129141
</dd>
130142
</div>
131-
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
143+
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
132144
<dt className="text-sm font-medium text-gray-500">Maintenance</dt>
133145
<dd className="mt-1 sm:mt-0 sm:col-span-2">
134146
{node.maintenance ? (
@@ -143,31 +155,31 @@ export default function NodeDetailPage({ params }: { params: Promise<{ uuid: str
143155
</dd>
144156
</div>
145157
{node.driver && (
146-
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
158+
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
147159
<dt className="text-sm font-medium text-gray-500">Driver</dt>
148160
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{node.driver}</dd>
149161
</div>
150162
)}
151163
{node.resource_class && (
152-
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
164+
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
153165
<dt className="text-sm font-medium text-gray-500">Resource Class</dt>
154166
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{node.resource_class}</dd>
155167
</div>
156168
)}
157169
{node.instance_uuid && (
158-
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
170+
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
159171
<dt className="text-sm font-medium text-gray-500">Instance UUID</dt>
160172
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{node.instance_uuid}</dd>
161173
</div>
162174
)}
163175
{node.created_at && (
164-
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
176+
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
165177
<dt className="text-sm font-medium text-gray-500">Created</dt>
166178
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{new Date(node.created_at).toLocaleString()}</dd>
167179
</div>
168180
)}
169181
{node.updated_at && (
170-
<div className="bg-white px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
182+
<div className="bg-gray-50 px-4 py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
171183
<dt className="text-sm font-medium text-gray-500">Updated</dt>
172184
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{new Date(node.updated_at).toLocaleString()}</dd>
173185
</div>

frontend/app/nodes/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ export default function NodesPage() {
239239
<p className="text-sm font-medium text-gray-900">
240240
{node.name || node.uuid}
241241
</p>
242+
{node.device_role && (
243+
<span className="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">
244+
{node.device_role}
245+
</span>
246+
)}
242247
{node.maintenance && (
243248
<span className="ml-2 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
244249
Maintenance

frontend/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface BaremetalNode {
22
uuid: string;
33
name: string | null;
4+
device_role: string | null;
45
power_state: string | null;
56
provision_state: string | null;
67
maintenance: boolean | null;

osism/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ class BaremetalNode(BaseModel):
142142
instance_uuid: Optional[str] = Field(
143143
None, description="UUID of associated instance"
144144
)
145+
device_role: Optional[str] = Field(None, description="Device role from NetBox")
145146
driver: Optional[str] = Field(None, description="Driver used for the node")
146147
resource_class: Optional[str] = Field(
147148
None, description="Resource class of the node"

osism/tasks/openstack.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,28 @@ def get_baremetal_nodes():
9090
node_list = []
9191
for node in nodes:
9292
# OpenStack SDK returns Resource objects, not dicts - use attribute access
93+
node_name = getattr(node, "name", None)
94+
95+
# Get device role from NetBox
96+
device_role = None
97+
if utils.nb and node_name:
98+
try:
99+
device = utils.nb.dcim.devices.get(name=node_name)
100+
if not device:
101+
devices = utils.nb.dcim.devices.filter(
102+
cf_inventory_hostname=node_name
103+
)
104+
if devices:
105+
device = list(devices)[0]
106+
if device and device.role and hasattr(device.role, "name"):
107+
device_role = device.role.name
108+
except Exception as e:
109+
logger.debug(f"Could not get device role for {node_name}: {e}")
110+
93111
node_info = {
94112
"uuid": getattr(node, "uuid", None) or getattr(node, "id", None),
95-
"name": getattr(node, "name", None),
113+
"name": node_name,
114+
"device_role": device_role,
96115
"power_state": getattr(node, "power_state", None),
97116
"provision_state": getattr(node, "provision_state", None),
98117
"maintenance": getattr(node, "maintenance", None),

0 commit comments

Comments
 (0)