osism 0.20250602.0__py3-none-any.whl → 0.20250616.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.
@@ -0,0 +1,61 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ import copy
4
+ from celery import Celery
5
+ from celery.signals import worker_process_init
6
+ from loguru import logger
7
+
8
+ from osism.tasks import Config
9
+ from osism.tasks.conductor.config import get_configuration
10
+ from osism.tasks.conductor.ironic import sync_ironic as _sync_ironic
11
+ from osism.tasks.conductor.sonic import sync_sonic as _sync_sonic
12
+
13
+
14
+ # App configuration
15
+ app = Celery("conductor")
16
+ app.config_from_object(Config)
17
+
18
+
19
+ @worker_process_init.connect
20
+ def celery_init_worker(**kwargs):
21
+ pass
22
+
23
+
24
+ @app.on_after_configure.connect
25
+ def setup_periodic_tasks(sender, **kwargs):
26
+ pass
27
+
28
+
29
+ # Tasks
30
+ @app.task(bind=True, name="osism.tasks.conductor.get_ironic_parameters")
31
+ def get_ironic_parameters(self):
32
+ configuration = get_configuration()
33
+ if "ironic_parameters" in configuration:
34
+ # NOTE: Do not pass by reference, everybody gets their own copy to work with
35
+ return copy.deepcopy(configuration["ironic_parameters"])
36
+
37
+ return {}
38
+
39
+
40
+ @app.task(bind=True, name="osism.tasks.conductor.sync_netbox")
41
+ def sync_netbox(self, force_update=False):
42
+ logger.info("Not implemented")
43
+
44
+
45
+ @app.task(bind=True, name="osism.tasks.conductor.sync_ironic")
46
+ def sync_ironic(self, force_update=False):
47
+ _sync_ironic(self.request.id, get_ironic_parameters, force_update)
48
+
49
+
50
+ @app.task(bind=True, name="osism.tasks.conductor.sync_sonic")
51
+ def sync_sonic(self):
52
+ return _sync_sonic()
53
+
54
+
55
+ __all__ = [
56
+ "app",
57
+ "get_ironic_parameters",
58
+ "sync_netbox",
59
+ "sync_ironic",
60
+ "sync_sonic",
61
+ ]
@@ -0,0 +1,92 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ import uuid
4
+
5
+ from loguru import logger
6
+ import yaml
7
+
8
+ from osism.tasks import Config, openstack
9
+
10
+
11
+ def is_uuid(value):
12
+ """Check if a string is a valid UUID."""
13
+ try:
14
+ uuid.UUID(value)
15
+ return True
16
+ except (ValueError, AttributeError):
17
+ return False
18
+
19
+
20
+ def get_configuration():
21
+ with open("/etc/conductor.yml") as fp:
22
+ configuration = yaml.load(fp, Loader=yaml.SafeLoader)
23
+
24
+ if not configuration:
25
+ logger.warning(
26
+ "The conductor configuration is empty. That's probably wrong"
27
+ )
28
+ return {}
29
+
30
+ if Config.enable_ironic.lower() not in ["true", "yes"]:
31
+ return configuration
32
+
33
+ if "ironic_parameters" not in configuration:
34
+ logger.error("ironic_parameters not found in the conductor configuration")
35
+ return configuration
36
+
37
+ if "instance_info" in configuration["ironic_parameters"]:
38
+ if "image_source" in configuration["ironic_parameters"]["instance_info"]:
39
+ image_source = configuration["ironic_parameters"]["instance_info"][
40
+ "image_source"
41
+ ]
42
+ if not is_uuid(image_source):
43
+ result = openstack.image_get(image_source)
44
+ configuration["ironic_parameters"]["instance_info"][
45
+ "image_source"
46
+ ] = result.id
47
+
48
+ if "driver_info" in configuration["ironic_parameters"]:
49
+ if "deploy_kernel" in configuration["ironic_parameters"]["driver_info"]:
50
+ deploy_kernel = configuration["ironic_parameters"]["driver_info"][
51
+ "deploy_kernel"
52
+ ]
53
+ if not is_uuid(deploy_kernel):
54
+ result = openstack.image_get(deploy_kernel)
55
+ configuration["ironic_parameters"]["driver_info"][
56
+ "deploy_kernel"
57
+ ] = result.id
58
+
59
+ if "deploy_ramdisk" in configuration["ironic_parameters"]["driver_info"]:
60
+ deploy_ramdisk = configuration["ironic_parameters"]["driver_info"][
61
+ "deploy_ramdisk"
62
+ ]
63
+ if not is_uuid(deploy_ramdisk):
64
+ result = openstack.image_get(deploy_ramdisk)
65
+ configuration["ironic_parameters"]["driver_info"][
66
+ "deploy_ramdisk"
67
+ ] = result.id
68
+
69
+ if "cleaning_network" in configuration["ironic_parameters"]["driver_info"]:
70
+ result = openstack.network_get(
71
+ configuration["ironic_parameters"]["driver_info"][
72
+ "cleaning_network"
73
+ ]
74
+ )
75
+ configuration["ironic_parameters"]["driver_info"][
76
+ "cleaning_network"
77
+ ] = result.id
78
+
79
+ if (
80
+ "provisioning_network"
81
+ in configuration["ironic_parameters"]["driver_info"]
82
+ ):
83
+ result = openstack.network_get(
84
+ configuration["ironic_parameters"]["driver_info"][
85
+ "provisioning_network"
86
+ ]
87
+ )
88
+ configuration["ironic_parameters"]["driver_info"][
89
+ "provisioning_network"
90
+ ] = result.id
91
+
92
+ return configuration
@@ -0,0 +1,343 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ import json
4
+
5
+ import jinja2
6
+ from pottery import Redlock
7
+
8
+ from osism import utils as osism_utils
9
+ from osism.tasks import netbox, openstack
10
+ from osism.tasks.conductor.netbox import (
11
+ get_device_oob_ip,
12
+ get_nb_device_query_list_ironic,
13
+ )
14
+ from osism.tasks.conductor.utils import (
15
+ deep_compare,
16
+ deep_decrypt,
17
+ deep_merge,
18
+ get_vault,
19
+ )
20
+
21
+
22
+ driver_params = {
23
+ "ipmi": {
24
+ "address": "ipmi_address",
25
+ "port": "ipmi_port",
26
+ "password": "ipmi_password",
27
+ "username": "ipmi_username",
28
+ },
29
+ "redfish": {
30
+ "address": "redfish_address",
31
+ "password": "redfish_password",
32
+ "username": "redfish_username",
33
+ },
34
+ }
35
+
36
+
37
+ def sync_ironic(request_id, get_ironic_parameters, force_update=False):
38
+ osism_utils.push_task_output(
39
+ request_id,
40
+ "Starting NetBox device synchronisation with ironic\n",
41
+ )
42
+ devices = set()
43
+ nb_device_query_list = get_nb_device_query_list_ironic()
44
+ for nb_device_query in nb_device_query_list:
45
+ devices |= set(netbox.get_devices(**nb_device_query))
46
+
47
+ # NOTE: Find nodes in Ironic which are no longer present in NetBox and remove them
48
+ device_names = {dev.name for dev in devices}
49
+ nodes = openstack.baremetal_node_list()
50
+ for node in nodes:
51
+ osism_utils.push_task_output(
52
+ request_id, f"Looking for {node['Name']} in NetBox\n"
53
+ )
54
+ if node["Name"] not in device_names:
55
+ if (
56
+ not node["Instance UUID"]
57
+ and node["Provisioning State"] in ["enroll", "manageable", "available"]
58
+ and node["Power State"] in ["power off", None]
59
+ ):
60
+ osism_utils.push_task_output(
61
+ request_id,
62
+ f"Cleaning up baremetal node not found in NetBox: {node['Name']}\n",
63
+ )
64
+ for port in openstack.baremetal_port_list(
65
+ details=False, attributes=dict(node_uuid=node["UUID"])
66
+ ):
67
+ openstack.baremetal_port_delete(port.id)
68
+ openstack.baremetal_node_delete(node["UUID"])
69
+ else:
70
+ osism_utils.push_task_output(
71
+ f"Cannot remove baremetal node because it is still provisioned or running: {node}"
72
+ )
73
+
74
+ # NOTE: Find nodes in NetBox which are not present in Ironic and add them
75
+ for device in devices:
76
+ osism_utils.push_task_output(
77
+ request_id, f"Looking for {device.name} in ironic\n"
78
+ )
79
+
80
+ node_interfaces = list(netbox.get_interfaces_by_device(device.name))
81
+
82
+ node_attributes = get_ironic_parameters()
83
+ if (
84
+ "ironic_parameters" in device.custom_fields
85
+ and device.custom_fields["ironic_parameters"]
86
+ ):
87
+ # NOTE: Update node attributes with overrides from NetBox device
88
+ deep_merge(node_attributes, device.custom_fields["ironic_parameters"])
89
+
90
+ # NOTE: Decrypt ansible vaulted secrets
91
+ vault = get_vault()
92
+ deep_decrypt(node_attributes, vault)
93
+
94
+ node_secrets = device.custom_fields.get("secrets", {})
95
+ if node_secrets is None:
96
+ node_secrets = {}
97
+ deep_decrypt(node_secrets, vault)
98
+
99
+ if (
100
+ "driver" in node_attributes
101
+ and node_attributes["driver"] in driver_params.keys()
102
+ ):
103
+ if "driver_info" in node_attributes:
104
+ # NOTE: Remove all fields belonging to a different driver
105
+ unused_drivers = [
106
+ driver
107
+ for driver in driver_params.keys()
108
+ if driver != node_attributes["driver"]
109
+ ]
110
+ for key in list(node_attributes["driver_info"].keys()):
111
+ for driver in unused_drivers:
112
+ if key.startswith(driver + "_"):
113
+ node_attributes["driver_info"].pop(key, None)
114
+
115
+ # NOTE: Render driver username field
116
+ username_key = driver_params[node_attributes["driver"]]["username"]
117
+ if username_key in node_attributes["driver_info"]:
118
+ node_attributes["driver_info"][username_key] = (
119
+ jinja2.Environment(loader=jinja2.BaseLoader())
120
+ .from_string(node_attributes["driver_info"][username_key])
121
+ .render(
122
+ remote_board_username=str(
123
+ node_secrets.get("remote_board_username", "admin")
124
+ )
125
+ )
126
+ )
127
+
128
+ # NOTE: Render driver password field
129
+ password_key = driver_params[node_attributes["driver"]]["password"]
130
+ if password_key in node_attributes["driver_info"]:
131
+ node_attributes["driver_info"][password_key] = (
132
+ jinja2.Environment(loader=jinja2.BaseLoader())
133
+ .from_string(node_attributes["driver_info"][password_key])
134
+ .render(
135
+ remote_board_password=str(
136
+ node_secrets.get("remote_board_password", "password")
137
+ )
138
+ )
139
+ )
140
+
141
+ # NOTE: Render driver address field
142
+ address_key = driver_params[node_attributes["driver"]]["address"]
143
+ if address_key in node_attributes["driver_info"]:
144
+ oob_ip_result = get_device_oob_ip(device)
145
+ if oob_ip_result:
146
+ oob_ip, _ = (
147
+ oob_ip_result # Extract IP address, ignore prefix length
148
+ )
149
+ node_attributes["driver_info"][address_key] = (
150
+ jinja2.Environment(loader=jinja2.BaseLoader())
151
+ .from_string(node_attributes["driver_info"][address_key])
152
+ .render(remote_board_address=oob_ip)
153
+ )
154
+ node_attributes.update({"resource_class": device.name})
155
+ # NOTE: Write metadata used for provisioning into 'extra' field, so that
156
+ # it is available during node deploy without querying the NetBox again
157
+ if "extra" not in node_attributes:
158
+ node_attributes["extra"] = {}
159
+ if (
160
+ "netplan_parameters" in device.custom_fields
161
+ and device.custom_fields["netplan_parameters"]
162
+ ):
163
+ node_attributes["extra"].update(
164
+ {
165
+ "netplan_parameters": json.dumps(
166
+ device.custom_fields["netplan_parameters"]
167
+ )
168
+ }
169
+ )
170
+ if (
171
+ "frr_parameters" in device.custom_fields
172
+ and device.custom_fields["frr_parameters"]
173
+ ):
174
+ node_attributes["extra"].update(
175
+ {"frr_parameters": json.dumps(device.custom_fields["frr_parameters"])}
176
+ )
177
+ ports_attributes = [
178
+ dict(address=interface.mac_address)
179
+ for interface in node_interfaces
180
+ if interface.enabled and not interface.mgmt_only and interface.mac_address
181
+ ]
182
+
183
+ lock = Redlock(
184
+ key=f"lock_osism_tasks_conductor_sync_ironic-{device.name}",
185
+ masters={osism_utils.redis},
186
+ auto_release_time=600,
187
+ )
188
+ if lock.acquire(timeout=120):
189
+ try:
190
+ osism_utils.push_task_output(
191
+ request_id, f"Processing device {device.name}\n"
192
+ )
193
+ node = openstack.baremetal_node_show(device.name, ignore_missing=True)
194
+ if not node:
195
+ osism_utils.push_task_output(
196
+ request_id, f"Creating baremetal node for {device.name}\n"
197
+ )
198
+ node = openstack.baremetal_node_create(device.name, node_attributes)
199
+ else:
200
+ # NOTE: The listener service only reacts to changes in the baremetal node. Explicitly sync provision and power state in case updates were missed by the listener.
201
+ if (
202
+ device.custom_fields["provision_state"]
203
+ != node["provision_state"]
204
+ ):
205
+ netbox.set_provision_state(device.name, node["provision_state"])
206
+ if device.custom_fields["power_state"] != node["power_state"]:
207
+ netbox.set_power_state(device.name, node["power_state"])
208
+ # NOTE: Check whether the baremetal node needs to be updated
209
+ node_updates = {}
210
+ deep_compare(node_attributes, node, node_updates)
211
+ if "driver_info" in node_updates:
212
+ # NOTE: The password is not returned by ironic, so we cannot make a comparision and it would always be updated. Therefore we pop it from the dictionary
213
+ password_key = driver_params[node_attributes["driver"]][
214
+ "password"
215
+ ]
216
+ if password_key in node_updates["driver_info"]:
217
+ node_updates["driver_info"].pop(password_key, None)
218
+ if not node_updates["driver_info"]:
219
+ node_updates.pop("driver_info", None)
220
+ if node_updates or force_update:
221
+ osism_utils.push_task_output(
222
+ request_id,
223
+ f"Updating baremetal node for {device.name} with {node_updates}\n",
224
+ )
225
+ # NOTE: Do the actual updates with all values in node_attributes. Otherwise nested dicts like e.g. driver_info will be overwritten as a whole and contain only changed values
226
+ node = openstack.baremetal_node_update(
227
+ node["uuid"], node_attributes
228
+ )
229
+
230
+ node_ports = openstack.baremetal_port_list(
231
+ details=False, attributes=dict(node_uuid=node["uuid"])
232
+ )
233
+ # NOTE: Baremetal ports are only required for (i)pxe boot
234
+ if node["boot_interface"] in ["pxe", "ipxe"]:
235
+ for port_attributes in ports_attributes:
236
+ port_attributes.update({"node_id": node["uuid"]})
237
+ port = [
238
+ port
239
+ for port in node_ports
240
+ if port_attributes["address"].upper()
241
+ == port["address"].upper()
242
+ ]
243
+ if not port:
244
+ osism_utils.push_task_output(
245
+ request_id,
246
+ f"Creating baremetal port with MAC address {port_attributes['address']} for {device.name}\n",
247
+ )
248
+ openstack.baremetal_port_create(port_attributes)
249
+ else:
250
+ node_ports.remove(port[0])
251
+ for node_port in node_ports:
252
+ # NOTE: Delete remaining ports not found in NetBox
253
+ osism_utils.push_task_output(
254
+ request_id,
255
+ f"Deleting baremetal port with MAC address {node_port['address']} for {device.name}\n",
256
+ )
257
+ openstack.baremetal_port_delete(node_port["id"])
258
+
259
+ node_validation = openstack.baremetal_node_validate(node["uuid"])
260
+ if node_validation["management"].result:
261
+ osism_utils.push_task_output(
262
+ request_id,
263
+ f"Validation of management interface successful for baremetal node for {device.name}\n",
264
+ )
265
+ if node["provision_state"] == "enroll":
266
+ osism_utils.push_task_output(
267
+ request_id,
268
+ f"Transitioning baremetal node to manageable state for {device.name}\n",
269
+ )
270
+ node = openstack.baremetal_node_set_provision_state(
271
+ node["uuid"], "manage"
272
+ )
273
+ node = openstack.baremetal_node_wait_for_nodes_provision_state(
274
+ node["uuid"], "manageable"
275
+ )
276
+ osism_utils.push_task_output(
277
+ request_id,
278
+ f"Baremetal node for {device.name} is manageable\n",
279
+ )
280
+ if node_validation["boot"].result:
281
+ osism_utils.push_task_output(
282
+ request_id,
283
+ f"Validation of boot interface successful for baremetal node for {device.name}\n",
284
+ )
285
+ if node["provision_state"] == "manageable":
286
+ osism_utils.push_task_output(
287
+ request_id,
288
+ f"Transitioning baremetal node to available state for {device.name}\n",
289
+ )
290
+ node = openstack.baremetal_node_set_provision_state(
291
+ node["uuid"], "provide"
292
+ )
293
+ node = (
294
+ openstack.baremetal_node_wait_for_nodes_provision_state(
295
+ node["uuid"], "available"
296
+ )
297
+ )
298
+ osism_utils.push_task_output(
299
+ request_id,
300
+ f"Baremetal node for {device.name} is available\n",
301
+ )
302
+ else:
303
+ osism_utils.push_task_output(
304
+ request_id,
305
+ f"Validation of boot interface failed for baremetal node for {device.name}\nReason: {node_validation['boot'].reason}\n",
306
+ )
307
+ if node["provision_state"] == "available":
308
+ # NOTE: Demote node to manageable
309
+ osism_utils.push_task_output(
310
+ request_id,
311
+ f"Transitioning baremetal node to manageable state for {device.name}\n",
312
+ )
313
+ node = openstack.baremetal_node_set_provision_state(
314
+ node["uuid"], "manage"
315
+ )
316
+ node = (
317
+ openstack.baremetal_node_wait_for_nodes_provision_state(
318
+ node["uuid"], "manageable"
319
+ )
320
+ )
321
+ osism_utils.push_task_output(
322
+ request_id,
323
+ f"Baremetal node for {device.name} is manageable\n",
324
+ )
325
+ else:
326
+ osism_utils.push_task_output(
327
+ request_id,
328
+ f"Validation of management interface failed for baremetal node for {device.name}\nReason: {node_validation['management'].reason}\n",
329
+ )
330
+ except Exception as exc:
331
+ osism_utils.push_task_output(
332
+ request_id,
333
+ f"Could not fully synchronize device {device.name} with ironic: {exc}\n",
334
+ )
335
+ finally:
336
+ lock.release()
337
+
338
+ else:
339
+ osism_utils.push_task_output(
340
+ "Could not acquire lock for node {device.name}"
341
+ )
342
+
343
+ osism_utils.finish_task_output(request_id, rc=0)