osism 0.20250331.0__py3-none-any.whl → 0.20250425.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.
@@ -11,8 +11,7 @@ from loguru import logger
11
11
  import json
12
12
  import requests
13
13
 
14
- from osism import utils
15
- from osism.tasks import netbox, openstack
14
+ from osism.tasks import netbox
16
15
  from osism import settings
17
16
 
18
17
  EXCHANGE_NAME = "ironic"
@@ -40,16 +39,13 @@ class BaremetalEvents:
40
39
  },
41
40
  "maintenance_set": {"end": self.node_maintenance_set_end},
42
41
  "provision_set": {
42
+ "start": self.node_provision_set_start,
43
43
  "end": self.node_provision_set_end,
44
44
  "success": self.node_provision_set_success,
45
45
  },
46
46
  "delete": {"end": self.node_delete_end},
47
47
  "create": {"end": self.node_create_end},
48
48
  },
49
- "port": {
50
- "create": {"end": self.port_create_end},
51
- "update": {"end": self.port_update_end},
52
- },
53
49
  }
54
50
  }
55
51
 
@@ -78,7 +74,7 @@ class BaremetalEvents:
78
74
  logger.info(
79
75
  f"baremetal.node.power_set.end ## {name} ## {object_data['power_state']}"
80
76
  )
81
- netbox.set_state.delay(name, object_data["power_state"], "power")
77
+ netbox.set_power_state.delay(name, object_data["power_state"])
82
78
 
83
79
  def node_power_state_corrected_success(self, payload: dict[Any, Any]) -> None:
84
80
  object_data = self.get_object_data(payload)
@@ -86,7 +82,7 @@ class BaremetalEvents:
86
82
  logger.info(
87
83
  f"baremetal.node.power_state_corrected.success ## {name} ## {object_data['power_state']}"
88
84
  )
89
- netbox.set_state.delay(name, object_data["power_state"], "power")
85
+ netbox.set_power_state.delay(name, object_data["power_state"])
90
86
 
91
87
  def node_maintenance_set_end(self, payload: dict[Any, Any]) -> None:
92
88
  object_data = self.get_object_data(payload)
@@ -94,7 +90,7 @@ class BaremetalEvents:
94
90
  logger.info(
95
91
  f"baremetal.node.maintenance_set.end ## {name} ## {object_data['maintenance']}"
96
92
  )
97
- netbox.set_maintenance.delay(name, object_data["maintenance"])
93
+ netbox.set_maintenance.delay(name, state=object_data["maintenance"])
98
94
 
99
95
  def node_provision_set_success(self, payload: dict[Any, Any]) -> None:
100
96
  # A provision status was successfully set, update it in the netbox
@@ -103,80 +99,37 @@ class BaremetalEvents:
103
99
  logger.info(
104
100
  f"baremetal.node.provision_set.success ## {name} ## {object_data['provision_state']}"
105
101
  )
106
- netbox.set_state.delay(name, object_data["provision_state"], "provision")
102
+ netbox.set_provision_state.delay(name, object_data["provision_state"])
107
103
 
108
- def node_provision_set_end(self, payload: dict[Any, Any]) -> None:
104
+ def node_provision_set_start(self, payload: dict[Any, Any]) -> None:
109
105
  object_data = self.get_object_data(payload)
110
106
  name = object_data["name"]
111
107
  logger.info(
112
- f"baremetal.node.provision_set.end ## {name} ## {object_data['provision_state']}"
113
- )
114
- netbox.set_state.delay(name, object_data["provision_state"], "provision")
115
-
116
- if (
117
- object_data["previous_provision_state"] == "inspect wait"
118
- and object_data["event"] == "done"
119
- ):
120
- netbox.set_state.delay(name, "introspected", "introspection")
121
- openstack.baremetal_set_node_provision_state.delay(name, "provide")
122
-
123
- def port_create_end(self, payload: dict[Any, Any]) -> None:
124
- object_data = self.get_object_data(payload)
125
- name = object_data["name"]
126
- logger.info(f"baremetal.port.create.end ## {object_data['uuid']}")
127
-
128
- mac_address = object_data["address"]
129
- interface_a = utils.nb.dcim.interfaces.get(mac_address=mac_address)
130
- device_a = interface_a.device
131
-
132
- task = openstack.baremetal_get_network_interface_name.delay(
133
- device_a.name, mac_address
134
- )
135
- task.wait(timeout=None, interval=0.5)
136
- network_interface_name = task.get()
137
-
138
- netbox.update_network_interface_name.delay(
139
- object_data["address"], network_interface_name
108
+ f"baremetal.node.provision_set.start ## {name} ## {object_data['provision_state']}"
140
109
  )
110
+ netbox.set_provision_state.delay(name, object_data["provision_state"])
141
111
 
142
- def port_update_end(self, payload: dict[Any, Any]) -> None:
112
+ def node_provision_set_end(self, payload: dict[Any, Any]) -> None:
143
113
  object_data = self.get_object_data(payload)
144
114
  name = object_data["name"]
145
- logger.info(f"baremetal.port.update.end ## {object_data['uuid']}")
146
-
147
- mac_address = object_data["address"]
148
- interface_a = utils.nb.dcim.interfaces.get(mac_address=mac_address)
149
- device_a = interface_a.device
150
-
151
- task = openstack.baremetal_get_network_interface_name.delay(
152
- device_a.name, mac_address
153
- )
154
- task.wait(timeout=None, interval=0.5)
155
- network_interface_name = task.get()
156
-
157
- netbox.update_network_interface_name.delay(
158
- object_data["address"], network_interface_name
115
+ logger.info(
116
+ f"baremetal.node.provision_set.end ## {name} ## {object_data['provision_state']}"
159
117
  )
118
+ netbox.set_provision_state.delay(name, object_data["provision_state"])
160
119
 
161
120
  def node_delete_end(self, payload: dict[Any, Any]) -> None:
162
121
  object_data = self.get_object_data(payload)
163
122
  name = object_data["name"]
164
123
  logger.info(f"baremetal.node.delete.end ## {name}")
165
-
166
- netbox.set_state.delay(name, "unregistered", "ironic")
167
- netbox.set_state.delay(name, None, "provision")
168
- netbox.set_state.delay(name, None, "power")
169
- netbox.set_state.delay(name, None, "introspection")
170
- netbox.set_state.delay(name, None, "deployment")
171
-
172
- # remove internal flavor
173
- openstack.baremetal_delete_internal_flavor.delay(name)
124
+ netbox.set_provision_state.delay(name, None)
125
+ netbox.set_power_state.delay(name, None)
174
126
 
175
127
  def node_create_end(self, payload: dict[Any, Any]) -> None:
176
128
  object_data = self.get_object_data(payload)
177
129
  name = object_data["name"]
178
130
  logger.info(f"baremetal.node.create.end ## {name}")
179
- netbox.set_state.delay(name, "registered", "ironic")
131
+ netbox.set_provision_state.delay(name, object_data["provision_state"])
132
+ netbox.set_power_state.delay(name, object_data["power_state"])
180
133
 
181
134
 
182
135
  class NotificationsDump(ConsumerMixin):
@@ -207,7 +160,17 @@ class NotificationsDump(ConsumerMixin):
207
160
 
208
161
  def on_message(self, body, message):
209
162
  data = json.loads(body["oslo.message"])
210
- # logger.info(data)
163
+ logger.debug(
164
+ data["event_type"]
165
+ + ": "
166
+ + str(
167
+ {
168
+ k: v
169
+ for k, v in data["payload"]["ironic_object.data"].items()
170
+ if k in ["provision_state", "power_state"]
171
+ }
172
+ )
173
+ )
211
174
 
212
175
  if self.osism_api_session:
213
176
  tries = 1
@@ -268,8 +231,6 @@ class NotificationsDump(ConsumerMixin):
268
231
  handler = self.baremetal_events.get_handler(data["event_type"])
269
232
  handler(data["payload"])
270
233
 
271
- logger.info(self.baremetal_events.get_object_data(data["payload"]))
272
-
273
234
 
274
235
  def main():
275
236
  while True:
osism/settings.py CHANGED
@@ -34,3 +34,12 @@ INVENTORY_RECONCILER_SCHEDULE = float(
34
34
  )
35
35
 
36
36
  OSISM_API_URL = os.getenv("OSISM_API_URL", None)
37
+
38
+ OSISM_CONDUCTOR_NETBOX_FILTER_LIST = os.getenv(
39
+ "OSISM_CONDUCTOR_NETBOX_FILTER_LIST",
40
+ "[{'state': 'active', 'tag': ['managed-by-ironic']}]",
41
+ )
42
+
43
+ NETBOX_SECONDARIES = (
44
+ os.getenv("NETBOX_SECONDARIES", read_secret("NETBOX_SECONDARIES")) or "[]"
45
+ )
osism/tasks/__init__.py CHANGED
@@ -183,11 +183,16 @@ def run_command(
183
183
  *arguments,
184
184
  publish=True,
185
185
  locking=False,
186
+ ignore_env=False,
186
187
  auto_release_time=3600,
187
188
  ):
188
189
  result = ""
189
- command_env = os.environ.copy()
190
- command_env.update(env)
190
+
191
+ if ignore_env:
192
+ command_env = env
193
+ else:
194
+ command_env = os.environ.copy()
195
+ command_env.update(env)
191
196
 
192
197
  if locking:
193
198
  lock = Redlock(
osism/tasks/conductor.py CHANGED
@@ -2,30 +2,29 @@
2
2
 
3
3
  from celery import Celery
4
4
  from celery.signals import worker_process_init
5
- import keystoneauth1
5
+ import copy
6
+ import ipaddress
7
+ import jinja2
6
8
  from loguru import logger
7
- import openstack
9
+ from pottery import Redlock
8
10
  import yaml
9
11
 
10
- from osism.tasks import Config
12
+ from osism import settings
13
+ from osism import utils
14
+ from osism.tasks import Config, netbox, openstack
11
15
 
12
16
  app = Celery("conductor")
13
17
  app.config_from_object(Config)
14
18
 
15
19
 
16
20
  configuration = {}
21
+ nb_device_query_list = None
17
22
 
18
23
 
19
24
  @worker_process_init.connect
20
25
  def celery_init_worker(**kwargs):
21
26
  global configuration
22
27
 
23
- # Parameters come from the environment, OS_*
24
- try:
25
- conn = openstack.connect()
26
- except keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions:
27
- pass
28
-
29
28
  with open("/etc/conductor.yml") as fp:
30
29
  configuration = yaml.load(fp, Loader=yaml.SafeLoader)
31
30
 
@@ -33,20 +32,20 @@ def celery_init_worker(**kwargs):
33
32
  logger.warning(
34
33
  "The conductor configuration is empty. That's probably wrong"
35
34
  )
35
+ configuration = {}
36
36
  return
37
37
 
38
38
  # Resolve all IDs in the conductor.yml
39
- if Config.enable_ironic in ["True", "true", "Yes", "yes"]:
39
+ if Config.enable_ironic.lower() in ["true", "yes"]:
40
40
  if "ironic_parameters" not in configuration:
41
41
  logger.error(
42
42
  "ironic_parameters not found in the conductor configuration"
43
43
  )
44
44
  return
45
45
 
46
- # TODO: use osism.tasks.openstack in the future
47
46
  if "driver_info" in configuration["ironic_parameters"]:
48
47
  if "deploy_kernel" in configuration["ironic_parameters"]["driver_info"]:
49
- result = conn.image.find_image(
48
+ result = openstack.image_get(
50
49
  configuration["ironic_parameters"]["driver_info"][
51
50
  "deploy_kernel"
52
51
  ]
@@ -59,7 +58,7 @@ def celery_init_worker(**kwargs):
59
58
  "deploy_ramdisk"
60
59
  in configuration["ironic_parameters"]["driver_info"]
61
60
  ):
62
- result = conn.image.find_image(
61
+ result = openstack.image_get(
63
62
  configuration["ironic_parameters"]["driver_info"][
64
63
  "deploy_ramdisk"
65
64
  ]
@@ -72,7 +71,7 @@ def celery_init_worker(**kwargs):
72
71
  "cleaning_network"
73
72
  in configuration["ironic_parameters"]["driver_info"]
74
73
  ):
75
- result = conn.network.find_network(
74
+ result = openstack.network_get(
76
75
  configuration["ironic_parameters"]["driver_info"][
77
76
  "cleaning_network"
78
77
  ]
@@ -85,7 +84,7 @@ def celery_init_worker(**kwargs):
85
84
  "provisioning_network"
86
85
  in configuration["ironic_parameters"]["driver_info"]
87
86
  ):
88
- result = conn.network.find_network(
87
+ result = openstack.network_get(
89
88
  configuration["ironic_parameters"]["driver_info"][
90
89
  "provisioning_network"
91
90
  ]
@@ -94,6 +93,49 @@ def celery_init_worker(**kwargs):
94
93
  "provisioning_network"
95
94
  ] = result.id
96
95
 
96
+ global nb_device_query_list
97
+
98
+ try:
99
+ supported_nb_device_filters = [
100
+ "site",
101
+ "region",
102
+ "site_group",
103
+ "location",
104
+ "rack",
105
+ "tag",
106
+ "state",
107
+ ]
108
+ nb_device_query_list = yaml.safe_load(
109
+ settings.OSISM_CONDUCTOR_NETBOX_FILTER_LIST
110
+ )
111
+ if type(nb_device_query_list) is not list:
112
+ raise TypeError
113
+ for nb_device_query in nb_device_query_list:
114
+ if type(nb_device_query) is not dict:
115
+ raise TypeError
116
+ for key in list(nb_device_query.keys()):
117
+ if key not in supported_nb_device_filters:
118
+ raise ValueError
119
+ # NOTE: Only "location_id" and "rack_id" are supported by netbox
120
+ if key in ["location", "rack"]:
121
+ value_name = nb_device_query.pop(key, "")
122
+ if key == "location":
123
+ value_id = netbox.get_location_id(value_name)
124
+ elif key == "rack":
125
+ value_id = netbox.get_rack_id(value_name)
126
+ if value_id:
127
+ nb_device_query.update({key + "_id": value_id})
128
+ else:
129
+ raise ValueError(f"Invalid name {value_name} for {key}")
130
+ except (yaml.YAMLError, TypeError):
131
+ logger.error(
132
+ f"Setting OSISM_CONDUCTOR_NETBOX_FILTER_LIST needs to be an array of mappings containing supported netbox device filters: {supported_nb_device_filters}"
133
+ )
134
+ nb_device_query_list = []
135
+ except ValueError as exc:
136
+ logger.error(f"Unknown value in OSISM_CONDUCTOR_NETBOX_FILTER_LIST: {exc}")
137
+ nb_device_query_list = []
138
+
97
139
 
98
140
  @app.on_after_configure.connect
99
141
  def setup_periodic_tasks(sender, **kwargs):
@@ -103,6 +145,322 @@ def setup_periodic_tasks(sender, **kwargs):
103
145
  @app.task(bind=True, name="osism.tasks.conductor.get_ironic_parameters")
104
146
  def get_ironic_parameters(self):
105
147
  if "ironic_parameters" in configuration:
106
- return configuration["ironic_parameters"]
148
+ # NOTE: Do not pass by reference, everybody gets their own copy to work with
149
+ return copy.deepcopy(configuration["ironic_parameters"])
107
150
 
108
151
  return {}
152
+
153
+
154
+ @app.task(bind=True, name="osism.tasks.conductor.sync_netbox_with_ironic")
155
+ def sync_netbox_with_ironic(self, force_update=False):
156
+ def deep_compare(a, b, updates):
157
+ """
158
+ Find items in a that do not exist in b or are different.
159
+ Write required changes into updates
160
+ """
161
+ for key, value in a.items():
162
+ if type(value) is not dict:
163
+ if key not in b or b[key] != value:
164
+ updates[key] = value
165
+ else:
166
+ updates[key] = {}
167
+ deep_compare(a[key], b[key], updates[key])
168
+ if not updates[key]:
169
+ updates.pop(key)
170
+
171
+ driver_params = {
172
+ "ipmi": {
173
+ "address": "ipmi_address",
174
+ "port": "ipmi_port",
175
+ "password": "ipmi_password",
176
+ },
177
+ "redfish": {
178
+ "address": "redfish_address",
179
+ "password": "redfish_password",
180
+ },
181
+ }
182
+
183
+ devices = set()
184
+ for nb_device_query in nb_device_query_list:
185
+ devices |= set(netbox.get_devices(**nb_device_query))
186
+
187
+ # NOTE: Find nodes in Ironic which are no longer present in netbox and remove them
188
+ device_names = {dev.name for dev in devices}
189
+ nodes = openstack.baremetal_node_list()
190
+ for node in nodes:
191
+ logger.info(f"Looking for {node['Name']} in netbox")
192
+ if node["Name"] not in device_names:
193
+ if (
194
+ not node["Instance UUID"]
195
+ and node["Provisioning State"] in ["enroll", "manageable", "available"]
196
+ and node["Power State"] in ["power off", None]
197
+ ):
198
+ logger.info(
199
+ f"Cleaning up baremetal node not found in netbox: {node['Name']}"
200
+ )
201
+ flavor_name = "osism-" + node["Name"]
202
+ flavor = openstack.compute_flavor_get(flavor_name)
203
+ if flavor:
204
+ logger.info(f"Deleting flavor {flavor_name}")
205
+ openstack.compute_flavor_delete(flavor)
206
+ for port in openstack.baremetal_port_list(
207
+ details=False, attributes=dict(node_uuid=node["UUID"])
208
+ ):
209
+ openstack.baremetal_port_delete(port.id)
210
+ openstack.baremetal_node_delete(node["UUID"])
211
+ else:
212
+ logger.error(
213
+ f"Cannot remove baremetal node because it is still provisioned or running: {node}"
214
+ )
215
+
216
+ # NOTE: Find nodes in netbox which are not present in Ironic and add them
217
+ for device in devices:
218
+ logger.info(f"Looking for {device.name} in ironic")
219
+
220
+ node_interfaces = list(netbox.get_interfaces_by_device(device.name))
221
+
222
+ node_attributes = get_ironic_parameters()
223
+ if (
224
+ "driver" in node_attributes
225
+ and node_attributes["driver"] in driver_params.keys()
226
+ ):
227
+ if "driver_info" in node_attributes:
228
+ address_key = driver_params[node_attributes["driver"]]["address"]
229
+ if address_key in node_attributes["driver_info"]:
230
+ if "oob_address" in device.custom_fields:
231
+ node_mgmt_address = device.custom_fields["oob_address"]
232
+ elif "address" in device.oob_ip:
233
+ node_mgmt_address = device.oob_ip["address"]
234
+ else:
235
+ node_mgmt_addresses = [
236
+ interface["address"]
237
+ for interface in node_interfaces
238
+ if interface.mgmt_only
239
+ and "address" in interface
240
+ and interface["address"]
241
+ ]
242
+ if len(node_mgmt_addresses) > 0:
243
+ node_mgmt_address = node_mgmt_addresses[0]
244
+ else:
245
+ node_mgmt_address = None
246
+ if node_mgmt_address:
247
+ node_attributes["driver_info"][address_key] = (
248
+ jinja2.Environment(loader=jinja2.BaseLoader())
249
+ .from_string(node_attributes["driver_info"][address_key])
250
+ .render(
251
+ remote_board_address=str(
252
+ ipaddress.ip_interface(node_mgmt_address).ip
253
+ )
254
+ )
255
+ )
256
+ else:
257
+ logger.error(f"Could not find out-of-band address for {device}")
258
+ node_attributes["driver_info"].pop(address_key, None)
259
+ if (
260
+ "port" in driver_params[node_attributes["driver"]]
261
+ and "oob_port" in device.custom_fields
262
+ and device.custom_fields["oob_port"]
263
+ ):
264
+ port_key = driver_params[node_attributes["driver"]]["port"]
265
+ node_attributes["driver_info"].update(
266
+ {port_key: device.custom_fields["oob_port"]}
267
+ )
268
+ node_attributes.update({"resource_class": device.name})
269
+ ports_attributes = [
270
+ dict(address=interface.mac_address)
271
+ for interface in node_interfaces
272
+ if interface.enabled and not interface.mgmt_only and interface.mac_address
273
+ ]
274
+ flavor_attributes = {
275
+ "ram": 1,
276
+ "disk": 0,
277
+ "vcpus": 1,
278
+ "is_public": False,
279
+ "extra_specs": {
280
+ "resources:CUSTOM_"
281
+ + device.name.upper().replace("-", "_").replace(".", "_"): "1",
282
+ "resources:VCPU": "0",
283
+ "resources:MEMORY_MB": "0",
284
+ "resources:DISK_GB": "0",
285
+ },
286
+ }
287
+
288
+ lock = Redlock(
289
+ key=f"lock_osism_tasks_conductor_sync_netbox_with_ironic-{device.name}",
290
+ masters={utils.redis},
291
+ auto_release_time=60,
292
+ )
293
+ if lock.acquire(timeout=20):
294
+ try:
295
+ logger.info(f"Processing device {device.name}")
296
+ node = openstack.baremetal_node_show(device.name, ignore_missing=True)
297
+ if not node:
298
+ logger.info(f"Creating baremetal node for {device.name}")
299
+ node = openstack.baremetal_node_create(device.name, node_attributes)
300
+ else:
301
+ # 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.
302
+ if (
303
+ device.custom_fields["provision_state"]
304
+ != node["provision_state"]
305
+ ):
306
+ netbox.set_provision_state(device.name, node["provision_state"])
307
+ if device.custom_fields["power_state"] != node["power_state"]:
308
+ netbox.set_power_state(device.name, node["power_state"])
309
+ # NOTE: Check whether the baremetal node needs to be updated
310
+ node_updates = {}
311
+ deep_compare(node_attributes, node, node_updates)
312
+ if "driver_info" in node_updates:
313
+ # 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
314
+ password_key = driver_params[node_attributes["driver"]][
315
+ "password"
316
+ ]
317
+ if password_key in node_updates["driver_info"]:
318
+ node_updates["driver_info"].pop(password_key, None)
319
+ if not node_updates["driver_info"]:
320
+ node_updates.pop("driver_info", None)
321
+ if node_updates or force_update:
322
+ logger.info(
323
+ f"Updating baremetal node for {device.name} with {node_updates}"
324
+ )
325
+ # 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
326
+ node = openstack.baremetal_node_update(
327
+ node["uuid"], node_attributes
328
+ )
329
+
330
+ node_ports = openstack.baremetal_port_list(
331
+ details=False, attributes=dict(node_uuid=node["uuid"])
332
+ )
333
+ # NOTE: Baremetal ports are only required for (i)pxe boot
334
+ if node["boot_interface"] in ["pxe", "ipxe"]:
335
+ for port_attributes in ports_attributes:
336
+ port_attributes.update({"node_id": node["uuid"]})
337
+ port = [
338
+ port
339
+ for port in node_ports
340
+ if port_attributes["address"].upper()
341
+ == port["address"].upper()
342
+ ]
343
+ if not port:
344
+ logger.info(
345
+ f"Creating baremetal port with MAC address {port_attributes['address']} for {device.name}"
346
+ )
347
+ openstack.baremetal_port_create(port_attributes)
348
+ else:
349
+ node_ports.remove(port[0])
350
+ for node_port in node_ports:
351
+ # NOTE: Delete remaining ports not found in netbox
352
+ logger.info(
353
+ f"Deleting baremetal port with MAC address {node_port['address']} for {device.name}"
354
+ )
355
+ openstack.baremetal_port_delete(node_port["id"])
356
+
357
+ node_validation = openstack.baremetal_node_validate(node["uuid"])
358
+ if node_validation["management"].result:
359
+ logger.info(
360
+ f"Validation of management interface successful for baremetal node for {device.name}"
361
+ )
362
+ if node["provision_state"] == "enroll":
363
+ logger.info(
364
+ f"Transitioning baremetal node to manageable state for {device.name}"
365
+ )
366
+ node = openstack.baremetal_node_set_provision_state(
367
+ node["uuid"], "manage"
368
+ )
369
+ node = openstack.baremetal_node_wait_for_nodes_provision_state(
370
+ node["uuid"], "manageable"
371
+ )
372
+ logger.info(f"Baremetal node for {device.name} is manageable")
373
+ if node_validation["boot"].result:
374
+ logger.info(
375
+ f"Validation of boot interface successful for baremetal node for {device.name}"
376
+ )
377
+ if node["provision_state"] == "manageable":
378
+ logger.info(
379
+ f"Transitioning baremetal node to available state for {device.name}"
380
+ )
381
+ node = openstack.baremetal_node_set_provision_state(
382
+ node["uuid"], "provide"
383
+ )
384
+ node = (
385
+ openstack.baremetal_node_wait_for_nodes_provision_state(
386
+ node["uuid"], "available"
387
+ )
388
+ )
389
+ logger.info(
390
+ f"Baremetal node for {device.name} is available"
391
+ )
392
+ else:
393
+ logger.info(
394
+ f"Validation of boot interface failed for baremetal node for {device.name}\nReason: {node_validation['boot'].reason}"
395
+ )
396
+ if node["provision_state"] == "available":
397
+ # NOTE: Demote node to manageable
398
+ logger.info(
399
+ f"Transitioning baremetal node to manageable state for {device.name}"
400
+ )
401
+ node = openstack.baremetal_node_set_provision_state(
402
+ node["uuid"], "manage"
403
+ )
404
+ node = (
405
+ openstack.baremetal_node_wait_for_nodes_provision_state(
406
+ node["uuid"], "manageable"
407
+ )
408
+ )
409
+ logger.info(
410
+ f"Baremetal node for {device.name} is manageable"
411
+ )
412
+ else:
413
+ logger.info(
414
+ f"Validation of management interface failed for baremetal node for {device.name}\nReason: {node_validation['management'].reason}"
415
+ )
416
+
417
+ flavor_name = "osism-" + device.name
418
+ flavor = openstack.compute_flavor_get(flavor_name)
419
+ if not flavor:
420
+ logger.info(f"Creating flavor for {flavor_name}")
421
+ flavor = openstack.compute_flavor_create(
422
+ flavor_name, flavor_attributes
423
+ )
424
+ else:
425
+ flavor_updates = {}
426
+ deep_compare(flavor_attributes, flavor, flavor_updates)
427
+ flavor_updates_extra_specs = flavor_updates.pop("extra_specs", None)
428
+ if flavor_updates:
429
+ logger.info(
430
+ f"Updating flavor for {device.name} with {flavor_updates}"
431
+ )
432
+ openstack.compute_flavor_delete(flavor)
433
+ flavor = openstack.compute_flavor_create(
434
+ flavor_name, flavor_attributes
435
+ )
436
+ elif flavor_updates_extra_specs:
437
+ logger.info(
438
+ f"Updating flavor extra_specs for {device.name} with {flavor_updates_extra_specs}"
439
+ )
440
+ openstack.compute_flavor_update_extra_specs(
441
+ flavor, flavor_updates_extra_specs
442
+ )
443
+ flavor = openstack.compute_flavor_get(flavor_name)
444
+ for extra_specs_key in flavor["extra_specs"].keys():
445
+ if (
446
+ extra_specs_key
447
+ not in flavor_attributes["extra_specs"].keys()
448
+ ):
449
+ logger.info(
450
+ f"Deleting flavor extra_specs property {extra_specs_key} for {device.name}"
451
+ )
452
+ flavor = (
453
+ openstack.compute_flavor_delete_extra_specs_property(
454
+ flavor, extra_specs_key
455
+ )
456
+ )
457
+
458
+ except Exception as exc:
459
+ logger.info(
460
+ f"Could not fully synchronize device {device.name} with ironic: {exc}"
461
+ )
462
+ finally:
463
+ lock.release()
464
+
465
+ else:
466
+ logger.error("Could not acquire lock for node {device.name}")