osism 0.20250627.0__py3-none-any.whl → 0.20250701.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.
- osism/api.py +246 -120
- osism/commands/apply.py +2 -2
- osism/commands/baremetal.py +37 -6
- osism/commands/redfish.py +219 -0
- osism/commands/validate.py +1 -1
- osism/settings.py +3 -0
- osism/tasks/conductor/__init__.py +7 -0
- osism/tasks/conductor/config.py +52 -35
- osism/tasks/conductor/ironic.py +95 -99
- osism/tasks/conductor/redfish.py +300 -0
- osism/tasks/conductor/utils.py +148 -0
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/METADATA +3 -2
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/RECORD +21 -22
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/entry_points.txt +1 -0
- osism-0.20250701.0.dist-info/pbr.json +1 -0
- osism/actions/__init__.py +0 -1
- osism/core/__init__.py +0 -1
- osism/plugins/__init__.py +0 -1
- osism-0.20250627.0.dist-info/pbr.json +0 -1
- /osism/{core → data}/enums.py +0 -0
- /osism/{core → data}/playbooks.py +0 -0
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/WHEEL +0 -0
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/licenses/AUTHORS +0 -0
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250627.0.dist-info → osism-0.20250701.0.dist-info}/top_level.txt +0 -0
osism/api.py
CHANGED
@@ -3,10 +3,11 @@
|
|
3
3
|
import datetime
|
4
4
|
from logging.config import dictConfig
|
5
5
|
import logging
|
6
|
+
from typing import Optional, Dict, Any
|
6
7
|
from uuid import UUID
|
7
8
|
|
8
|
-
from fastapi import FastAPI, Header, Request, Response
|
9
|
-
from pydantic import BaseModel
|
9
|
+
from fastapi import FastAPI, Header, Request, Response, HTTPException, status
|
10
|
+
from pydantic import BaseModel, Field
|
10
11
|
from starlette.middleware.cors import CORSMiddleware
|
11
12
|
|
12
13
|
from osism.tasks import reconciler
|
@@ -15,186 +16,311 @@ from osism.services.listener import BaremetalEvents
|
|
15
16
|
|
16
17
|
|
17
18
|
class NotificationBaremetal(BaseModel):
|
18
|
-
priority: str
|
19
|
-
event_type: str
|
20
|
-
timestamp: str
|
21
|
-
publisher_id: str
|
22
|
-
message_id: UUID
|
23
|
-
payload:
|
19
|
+
priority: str = Field(..., description="Notification priority level")
|
20
|
+
event_type: str = Field(..., description="Type of the event")
|
21
|
+
timestamp: str = Field(..., description="Event timestamp")
|
22
|
+
publisher_id: str = Field(..., description="ID of the event publisher")
|
23
|
+
message_id: UUID = Field(..., description="Unique message identifier")
|
24
|
+
payload: Dict[str, Any] = Field(..., description="Event payload data")
|
24
25
|
|
25
26
|
|
26
27
|
class WebhookNetboxResponse(BaseModel):
|
27
|
-
result: str
|
28
|
+
result: str = Field(..., description="Operation result status")
|
28
29
|
|
29
30
|
|
30
31
|
class WebhookNetboxData(BaseModel):
|
31
|
-
username: str
|
32
|
-
data:
|
33
|
-
snapshots:
|
34
|
-
event: str
|
35
|
-
timestamp: datetime.datetime
|
36
|
-
model: str
|
37
|
-
request_id: UUID
|
32
|
+
username: str = Field(..., description="Username triggering the webhook")
|
33
|
+
data: Dict[str, Any] = Field(..., description="Webhook data payload")
|
34
|
+
snapshots: Dict[str, Any] = Field(..., description="Data snapshots")
|
35
|
+
event: str = Field(..., description="Event type")
|
36
|
+
timestamp: datetime.datetime = Field(..., description="Event timestamp")
|
37
|
+
model: str = Field(..., description="Model type")
|
38
|
+
request_id: UUID = Field(..., description="Unique request identifier")
|
38
39
|
|
39
40
|
|
40
|
-
# https://stackoverflow.com/questions/63510041/adding-python-logging-to-fastapi-endpoints-hosted-on-docker-doesnt-display-api
|
41
41
|
class LogConfig(BaseModel):
|
42
|
-
"""Logging configuration
|
42
|
+
"""Logging configuration for the OSISM API server."""
|
43
43
|
|
44
44
|
LOGGER_NAME: str = "osism"
|
45
|
-
LOG_FORMAT: str = "%(
|
46
|
-
LOG_LEVEL: str = "
|
45
|
+
LOG_FORMAT: str = "%(levelname)s | %(asctime)s | %(name)s | %(message)s"
|
46
|
+
LOG_LEVEL: str = "INFO"
|
47
47
|
|
48
48
|
# Logging config
|
49
49
|
version: int = 1
|
50
50
|
disable_existing_loggers: bool = False
|
51
|
-
formatters:
|
51
|
+
formatters: Dict[str, Any] = {
|
52
52
|
"default": {
|
53
|
-
"
|
54
|
-
"fmt": LOG_FORMAT,
|
53
|
+
"format": LOG_FORMAT,
|
55
54
|
"datefmt": "%Y-%m-%d %H:%M:%S",
|
56
55
|
},
|
57
56
|
}
|
58
|
-
handlers:
|
57
|
+
handlers: Dict[str, Any] = {
|
59
58
|
"default": {
|
60
59
|
"formatter": "default",
|
61
60
|
"class": "logging.StreamHandler",
|
62
61
|
"stream": "ext://sys.stderr",
|
63
62
|
},
|
64
63
|
}
|
65
|
-
loggers:
|
66
|
-
"
|
64
|
+
loggers: Dict[str, Any] = {
|
65
|
+
"osism": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False},
|
66
|
+
"api": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False},
|
67
|
+
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
68
|
+
"uvicorn.error": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
69
|
+
"uvicorn.access": {
|
70
|
+
"handlers": ["default"],
|
71
|
+
"level": "INFO",
|
72
|
+
"propagate": False,
|
73
|
+
},
|
67
74
|
}
|
68
75
|
|
69
76
|
|
70
|
-
app = FastAPI(
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
77
|
+
app = FastAPI(
|
78
|
+
title="OSISM API",
|
79
|
+
description="API for OpenStack Infrastructure & Service Manager",
|
80
|
+
version="1.0.0",
|
81
|
+
docs_url="/docs",
|
82
|
+
redoc_url="/redoc",
|
83
|
+
)
|
84
|
+
|
85
|
+
# Configure CORS - in production, replace with specific origins
|
86
|
+
app.add_middleware(
|
87
|
+
CORSMiddleware,
|
88
|
+
allow_origins=["*"], # TODO: Replace with actual allowed origins in production
|
89
|
+
allow_credentials=True,
|
90
|
+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
91
|
+
allow_headers=[
|
92
|
+
"Accept",
|
93
|
+
"Accept-Language",
|
94
|
+
"Content-Language",
|
95
|
+
"Content-Type",
|
96
|
+
"Authorization",
|
97
|
+
"X-Hook-Signature",
|
98
|
+
],
|
99
|
+
)
|
100
|
+
|
101
|
+
dictConfig(LogConfig().model_dump())
|
102
|
+
logger = logging.getLogger("osism.api")
|
76
103
|
|
77
104
|
baremetal_events = BaremetalEvents()
|
78
105
|
|
79
106
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
@app.get("/v1")
|
86
|
-
async def v1():
|
87
|
-
return {"result": "ok"}
|
88
|
-
|
89
|
-
|
90
|
-
@app.post("/v1/meters/sink")
|
91
|
-
async def write_sink_meters(request: Request):
|
92
|
-
data = await request.json()
|
93
|
-
|
94
|
-
|
95
|
-
@app.post("/v1/events/sink")
|
96
|
-
async def write_sink_events(request: Request):
|
97
|
-
data = await request.json()
|
98
|
-
|
99
|
-
|
100
|
-
@app.post("/v1/notifications/baremetal", status_code=204)
|
101
|
-
async def notifications_baremetal(notification: NotificationBaremetal) -> None:
|
102
|
-
|
103
|
-
handler = baremetal_events.get_handler(notification.event_type)
|
104
|
-
handler(notification.payload)
|
107
|
+
class DeviceSearchResult(BaseModel):
|
108
|
+
result: str = Field(..., description="Operation result status")
|
109
|
+
device: Optional[str] = Field(None, description="Device name if found")
|
105
110
|
|
106
111
|
|
107
|
-
|
108
|
-
|
112
|
+
def find_device_by_identifier(identifier: str):
|
113
|
+
"""Find a device in NetBox by various identifiers."""
|
109
114
|
if not utils.nb:
|
110
|
-
return
|
115
|
+
return None
|
111
116
|
|
112
117
|
device = None
|
113
118
|
|
114
119
|
# Search by device name
|
115
120
|
devices = utils.nb.dcim.devices.filter(name=identifier)
|
116
121
|
if devices:
|
117
|
-
device = devices[0]
|
122
|
+
device = list(devices)[0]
|
118
123
|
|
119
124
|
# Search by inventory_hostname custom field
|
120
125
|
if not device:
|
121
126
|
devices = utils.nb.dcim.devices.filter(cf_inventory_hostname=identifier)
|
122
127
|
if devices:
|
123
|
-
device = devices[0]
|
128
|
+
device = list(devices)[0]
|
124
129
|
|
125
130
|
# Search by serial number
|
126
131
|
if not device:
|
127
132
|
devices = utils.nb.dcim.devices.filter(serial=identifier)
|
128
133
|
if devices:
|
129
|
-
device = devices[0]
|
134
|
+
device = list(devices)[0]
|
130
135
|
|
131
|
-
|
136
|
+
return device
|
137
|
+
|
138
|
+
|
139
|
+
@app.get("/", tags=["health"])
|
140
|
+
async def root() -> Dict[str, str]:
|
141
|
+
"""Health check endpoint."""
|
142
|
+
return {"result": "ok"}
|
143
|
+
|
144
|
+
|
145
|
+
@app.get("/v1", tags=["health"])
|
146
|
+
async def v1() -> Dict[str, str]:
|
147
|
+
"""API version 1 health check endpoint."""
|
148
|
+
return {"result": "ok"}
|
149
|
+
|
150
|
+
|
151
|
+
class SinkResponse(BaseModel):
|
152
|
+
result: str = Field(..., description="Operation result status")
|
153
|
+
|
154
|
+
|
155
|
+
@app.post("/v1/meters/sink", response_model=SinkResponse, tags=["telemetry"])
|
156
|
+
async def write_sink_meters(request: Request) -> SinkResponse:
|
157
|
+
"""Write telemetry meters to sink."""
|
158
|
+
try:
|
159
|
+
data = await request.json()
|
160
|
+
# TODO: Implement meter processing logic
|
132
161
|
logger.info(
|
133
|
-
f"
|
162
|
+
f"Received meters data: {len(data) if isinstance(data, list) else 1} entries"
|
163
|
+
)
|
164
|
+
return SinkResponse(result="ok")
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(f"Error processing meters: {str(e)}")
|
167
|
+
raise HTTPException(
|
168
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
169
|
+
detail="Failed to process meters data",
|
134
170
|
)
|
135
171
|
|
136
|
-
# Set provision_state custom field to active
|
137
|
-
device.custom_fields["provision_state"] = "active"
|
138
|
-
device.save()
|
139
172
|
|
140
|
-
|
173
|
+
@app.post("/v1/events/sink", response_model=SinkResponse, tags=["telemetry"])
|
174
|
+
async def write_sink_events(request: Request) -> SinkResponse:
|
175
|
+
"""Write telemetry events to sink."""
|
176
|
+
try:
|
177
|
+
data = await request.json()
|
178
|
+
# TODO: Implement event processing logic
|
179
|
+
logger.info(
|
180
|
+
f"Received events data: {len(data) if isinstance(data, list) else 1} entries"
|
181
|
+
)
|
182
|
+
return SinkResponse(result="ok")
|
183
|
+
except Exception as e:
|
184
|
+
logger.error(f"Error processing events: {str(e)}")
|
185
|
+
raise HTTPException(
|
186
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
187
|
+
detail="Failed to process events data",
|
188
|
+
)
|
189
|
+
|
190
|
+
|
191
|
+
@app.post("/v1/notifications/baremetal", status_code=204, tags=["notifications"])
|
192
|
+
async def notifications_baremetal(notification: NotificationBaremetal) -> None:
|
193
|
+
"""Handle baremetal notifications."""
|
194
|
+
try:
|
195
|
+
handler = baremetal_events.get_handler(notification.event_type)
|
196
|
+
handler(notification.payload)
|
197
|
+
logger.info(
|
198
|
+
f"Successfully processed baremetal notification: {notification.event_type}"
|
199
|
+
)
|
200
|
+
except Exception as e:
|
201
|
+
logger.error(f"Error processing baremetal notification: {str(e)}")
|
202
|
+
if isinstance(e, HTTPException):
|
203
|
+
raise
|
204
|
+
raise HTTPException(
|
205
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
206
|
+
detail="Failed to process baremetal notification",
|
207
|
+
)
|
208
|
+
|
209
|
+
|
210
|
+
@app.post(
|
211
|
+
"/v1/switches/{identifier}/ztp/complete",
|
212
|
+
response_model=DeviceSearchResult,
|
213
|
+
tags=["switches"],
|
214
|
+
)
|
215
|
+
async def switches_ztp_complete(identifier: str) -> DeviceSearchResult:
|
216
|
+
"""Mark a switch as ZTP complete by setting provision_state to active."""
|
217
|
+
if not utils.nb:
|
218
|
+
raise HTTPException(
|
219
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
220
|
+
detail="NetBox is not enabled",
|
221
|
+
)
|
222
|
+
|
223
|
+
try:
|
224
|
+
device = find_device_by_identifier(identifier)
|
225
|
+
|
226
|
+
if device:
|
227
|
+
logger.info(
|
228
|
+
f"Found device {device.name} for ZTP complete with identifier {identifier}"
|
229
|
+
)
|
230
|
+
|
231
|
+
# Set provision_state custom field to active
|
232
|
+
device.custom_fields["provision_state"] = "active"
|
233
|
+
device.save()
|
234
|
+
|
235
|
+
return DeviceSearchResult(result="ok", device=device.name)
|
236
|
+
else:
|
237
|
+
logger.warning(
|
238
|
+
f"No device found for ZTP complete with identifier {identifier}"
|
239
|
+
)
|
240
|
+
raise HTTPException(
|
241
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
242
|
+
detail=f"Device not found with identifier: {identifier}",
|
243
|
+
)
|
244
|
+
except Exception as e:
|
245
|
+
logger.error(f"Error completing ZTP for device {identifier}: {str(e)}")
|
246
|
+
if isinstance(e, HTTPException):
|
247
|
+
raise
|
248
|
+
raise HTTPException(
|
249
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
250
|
+
detail="Failed to complete ZTP process",
|
251
|
+
)
|
252
|
+
|
253
|
+
|
254
|
+
def process_netbox_webhook(webhook_input: WebhookNetboxData) -> None:
|
255
|
+
"""Process NetBox webhook data."""
|
256
|
+
data = webhook_input.data
|
257
|
+
url = data["url"]
|
258
|
+
name = data["name"]
|
259
|
+
|
260
|
+
if "devices" in url:
|
261
|
+
tags = [x["name"] for x in data["tags"]]
|
262
|
+
custom_fields = data["custom_fields"]
|
263
|
+
device_type = custom_fields.get("device_type") or "node"
|
264
|
+
|
265
|
+
elif "interfaces" in url:
|
266
|
+
device_type = "interface"
|
267
|
+
device_id = data["device"]["id"]
|
268
|
+
device = utils.nb.dcim.devices.get(id=device_id)
|
269
|
+
tags = [str(x) for x in device.tags]
|
270
|
+
custom_fields = device.custom_fields
|
271
|
+
else:
|
272
|
+
logger.warning(f"Unknown webhook URL type: {url}")
|
273
|
+
return
|
274
|
+
|
275
|
+
if "Managed by OSISM" in tags:
|
276
|
+
if device_type == "server":
|
277
|
+
logger.info(
|
278
|
+
f"Handling change for managed device {name} of type {device_type}"
|
279
|
+
)
|
280
|
+
reconciler.run.delay()
|
281
|
+
elif device_type == "switch":
|
282
|
+
logger.info(
|
283
|
+
f"Handling change for managed device {name} of type {device_type}"
|
284
|
+
)
|
285
|
+
# TODO: Implement switch configuration generation
|
286
|
+
# netbox.generate.delay(name, custom_fields['configuration_template'])
|
287
|
+
elif device_type == "interface":
|
288
|
+
logger.info(
|
289
|
+
f"Handling change for interface {name} on managed device {device.name} of type {custom_fields['device_type']}"
|
290
|
+
)
|
291
|
+
# TODO: Implement interface configuration generation
|
292
|
+
# netbox.generate.delay(device.name, custom_fields['configuration_template'])
|
141
293
|
else:
|
142
|
-
logger.
|
143
|
-
return {"result": "device not found"}
|
294
|
+
logger.info(f"Ignoring change for unmanaged device {name}")
|
144
295
|
|
145
296
|
|
146
|
-
@app.post(
|
297
|
+
@app.post(
|
298
|
+
"/v1/webhook/netbox",
|
299
|
+
response_model=WebhookNetboxResponse,
|
300
|
+
status_code=200,
|
301
|
+
tags=["webhooks"],
|
302
|
+
)
|
147
303
|
async def webhook(
|
148
304
|
webhook_input: WebhookNetboxData,
|
149
305
|
request: Request,
|
150
306
|
response: Response,
|
151
307
|
content_length: int = Header(...),
|
152
|
-
x_hook_signature: str = Header(None),
|
153
|
-
):
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
tags = [x["name"] for x in data["tags"]]
|
161
|
-
|
162
|
-
custom_fields = data["custom_fields"]
|
163
|
-
device_type = custom_fields["device_type"]
|
164
|
-
|
165
|
-
# NOTE: device without a defined device_type are nodes
|
166
|
-
if not device_type:
|
167
|
-
device_type = "node"
|
168
|
-
|
169
|
-
elif "interfaces" in url:
|
170
|
-
device_type = "interface"
|
171
|
-
|
172
|
-
device_id = data["device"]["id"]
|
173
|
-
device = utils.nb.dcim.devices.get(id=device_id)
|
174
|
-
tags = [str(x) for x in device.tags]
|
175
|
-
custom_fields = device.custom_fields
|
176
|
-
|
177
|
-
if "Managed by OSISM" in tags:
|
178
|
-
if device_type == "server":
|
179
|
-
logger.info(
|
180
|
-
f"Handling change for managed device {name} of type {device_type}"
|
181
|
-
)
|
182
|
-
reconciler.run.delay()
|
183
|
-
elif device_type == "switch":
|
184
|
-
logger.info(
|
185
|
-
f"Handling change for managed device {name} of type {device_type}"
|
186
|
-
)
|
187
|
-
# netbox.generate.delay(name, custom_fields['configuration_template'])
|
188
|
-
elif device_type == "interface":
|
189
|
-
logger.info(
|
190
|
-
f"Handling change for interface {name} on managed device {device.name} of type {custom_fields['device_type']}"
|
191
|
-
)
|
192
|
-
# netbox.generate.delay(device.name, custom_fields['configuration_template'])
|
193
|
-
|
194
|
-
else:
|
195
|
-
logger.info(f"Ignoring change for unmanaged device {name}")
|
196
|
-
|
197
|
-
return {"result": "ok"}
|
308
|
+
x_hook_signature: Optional[str] = Header(None),
|
309
|
+
) -> WebhookNetboxResponse:
|
310
|
+
"""Handle NetBox webhook notifications."""
|
311
|
+
if not utils.nb:
|
312
|
+
raise HTTPException(
|
313
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
314
|
+
detail="NetBox webhook processing is not enabled",
|
315
|
+
)
|
198
316
|
|
199
|
-
|
200
|
-
|
317
|
+
try:
|
318
|
+
# TODO: Validate webhook signature if x_hook_signature is provided
|
319
|
+
process_netbox_webhook(webhook_input)
|
320
|
+
return WebhookNetboxResponse(result="ok")
|
321
|
+
except Exception as e:
|
322
|
+
logger.error(f"Error processing NetBox webhook: {str(e)}")
|
323
|
+
raise HTTPException(
|
324
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
325
|
+
detail="Failed to process NetBox webhook",
|
326
|
+
)
|
osism/commands/apply.py
CHANGED
@@ -9,8 +9,8 @@ from cliff.command import Command
|
|
9
9
|
from loguru import logger
|
10
10
|
from tabulate import tabulate
|
11
11
|
|
12
|
-
from osism.
|
13
|
-
from osism.
|
12
|
+
from osism.data import enums
|
13
|
+
from osism.data.playbooks import MAP_ROLE2ENVIRONMENT, MAP_ROLE2RUNTIME
|
14
14
|
from osism.tasks import ansible, ceph, kolla, kubernetes, handle_task
|
15
15
|
|
16
16
|
|
osism/commands/baremetal.py
CHANGED
@@ -12,6 +12,7 @@ import yaml
|
|
12
12
|
from openstack.baremetal import configdrive as configdrive_builder
|
13
13
|
|
14
14
|
from osism.commands import get_cloud_connection
|
15
|
+
from osism import utils
|
15
16
|
|
16
17
|
|
17
18
|
class BaremetalList(Command):
|
@@ -169,16 +170,51 @@ class BaremetalDeploy(Command):
|
|
169
170
|
continue
|
170
171
|
# NOTE: Prepare osism config drive
|
171
172
|
try:
|
173
|
+
# Get default vars from NetBox local_context_data if available
|
174
|
+
default_vars = {}
|
175
|
+
if utils.nb:
|
176
|
+
try:
|
177
|
+
# Try to find device by name first
|
178
|
+
device = utils.nb.dcim.devices.get(name=node.name)
|
179
|
+
|
180
|
+
# If not found by name, try by inventory_hostname custom field
|
181
|
+
if not device:
|
182
|
+
devices = utils.nb.dcim.devices.filter(
|
183
|
+
cf_inventory_hostname=node.name
|
184
|
+
)
|
185
|
+
if devices:
|
186
|
+
device = devices[0]
|
187
|
+
|
188
|
+
# Extract local_context_data if device found and has the field
|
189
|
+
if (
|
190
|
+
device
|
191
|
+
and hasattr(device, "local_context_data")
|
192
|
+
and device.local_context_data
|
193
|
+
):
|
194
|
+
default_vars = device.local_context_data
|
195
|
+
logger.info(
|
196
|
+
f"Using NetBox local_context_data for node {node.name}"
|
197
|
+
)
|
198
|
+
else:
|
199
|
+
logger.debug(
|
200
|
+
f"No local_context_data found for node {node.name} in NetBox"
|
201
|
+
)
|
202
|
+
except Exception as e:
|
203
|
+
logger.warning(
|
204
|
+
f"Failed to fetch NetBox data for node {node.name}: {e}"
|
205
|
+
)
|
206
|
+
|
172
207
|
playbook = []
|
173
208
|
play = {
|
174
209
|
"name": "Run bootstrap - part 2",
|
175
210
|
"hosts": "localhost",
|
176
211
|
"connection": "local",
|
177
212
|
"gather_facts": True,
|
178
|
-
"vars":
|
213
|
+
"vars": default_vars.copy(),
|
179
214
|
"roles": [
|
180
215
|
"osism.commons.hostname",
|
181
216
|
"osism.commons.hosts",
|
217
|
+
"osism.commons.operator",
|
182
218
|
],
|
183
219
|
}
|
184
220
|
play["vars"].update(
|
@@ -293,11 +329,6 @@ class BaremetalUndeploy(Command):
|
|
293
329
|
f"Node {node.name} ({node.id}) could not be moved to available state: {exc}"
|
294
330
|
)
|
295
331
|
continue
|
296
|
-
# NOTE: Ironic removes "instance_info" on undeploy. It was saved to "extra" during sync and needs to be refreshed here.
|
297
|
-
if "instance_info" in node["extra"]:
|
298
|
-
node = conn.baremetal.update_node(
|
299
|
-
node, instance_info=json.loads(node.extra["instance_info"])
|
300
|
-
)
|
301
332
|
else:
|
302
333
|
logger.warning(
|
303
334
|
f"Node {node.name} ({node.id}) not in supported provision state"
|