osism 0.20250331.0__py3-none-any.whl → 0.20250407.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/commands/manage.py CHANGED
@@ -12,7 +12,7 @@ import requests
12
12
  from osism.data import TEMPLATE_IMAGE_CLUSTERAPI, TEMPLATE_IMAGE_OCTAVIA
13
13
  from osism.tasks import openstack, handle_task
14
14
 
15
- SUPPORTED_CLUSTERAPI_K8S_IMAGES = ["1.29", "1.30", "1.31"]
15
+ SUPPORTED_CLUSTERAPI_K8S_IMAGES = ["1.30", "1.31", "1.32"]
16
16
 
17
17
 
18
18
  class ImageClusterapi(Command):
@@ -51,7 +51,7 @@ class ImageClusterapi(Command):
51
51
  parser.add_argument(
52
52
  "--filter",
53
53
  type=str,
54
- help="Filter the version to be managed (e.g. 1.31)",
54
+ help="Filter the version to be managed (e.g. 1.32)",
55
55
  default=None,
56
56
  )
57
57
  return parser
osism/commands/netbox.py CHANGED
@@ -3,29 +3,33 @@
3
3
  from cliff.command import Command
4
4
  from loguru import logger
5
5
 
6
- from osism.tasks import conductor, netbox, reconciler, openstack, handle_task
6
+ from osism.tasks import conductor, netbox, reconciler, handle_task
7
7
 
8
8
 
9
9
  class Ironic(Command):
10
10
  def get_parser(self, prog_name):
11
11
  parser = super(Ironic, self).get_parser(prog_name)
12
+ parser.add_argument(
13
+ "--no-wait",
14
+ help="Do not wait until the sync has been completed",
15
+ action="store_true",
16
+ )
17
+ parser.add_argument(
18
+ "--force-update",
19
+ help="Force update of baremetal nodes (Used to update non-comparable items like passwords)",
20
+ action="store_true",
21
+ )
12
22
  return parser
13
23
 
14
24
  def take_action(self, parsed_args):
15
- # Get Ironic parameters from the conductor
16
- task = conductor.get_ironic_parameters.delay()
17
- task.wait(timeout=None, interval=0.5)
18
- ironic_parameters = task.get()
25
+ wait = not parsed_args.no_wait
19
26
 
20
- # Add all unregistered systems from the Netbox in Ironic
21
- netbox.get_devices_not_yet_registered_in_ironic.apply_async(
22
- (), link=openstack.baremetal_create_nodes.s(ironic_parameters)
27
+ task = conductor.sync_netbox_with_ironic.delay(
28
+ force_update=parsed_args.force_update
23
29
  )
24
-
25
- # Synchronize the current status in Ironic with the Netbox
26
- # openstack.baremetal_node_list.apply_async((), link=netbox.synchronize_device_state.s())
27
-
28
- # Remove systems from Ironic that are no longer present in the Netbox
30
+ if wait:
31
+ logger.info(f"Task {task.task_id} is running. Wait. No more output.")
32
+ task.wait(timeout=None, interval=0.5)
29
33
 
30
34
 
31
35
  class Sync(Command):
@@ -33,7 +37,6 @@ class Sync(Command):
33
37
  parser = super(Sync, self).get_parser(prog_name)
34
38
  parser.add_argument(
35
39
  "--no-wait",
36
- default=False,
37
40
  help="Do not wait until the sync has been completed",
38
41
  action="store_true",
39
42
  )
osism/core/enums.py CHANGED
@@ -188,7 +188,7 @@ MAP_ROLE2ROLE = {
188
188
  [
189
189
  "common",
190
190
  [
191
- ["loadbalancer", ["opensearch", "mariadb-ng"]],
191
+ ["loadbalancer", ["letsencrypt", "opensearch", "mariadb-ng"]],
192
192
  ["openvswitch", ["ovn"]],
193
193
  "memcached",
194
194
  "redis",
@@ -199,6 +199,19 @@ MAP_ROLE2ROLE = {
199
199
  "collection-kubernetes": [
200
200
  ["kubernetes", ["kubeconfig", ["copy-kubeconfig"]]],
201
201
  ],
202
+ "collection-openstack-core": [
203
+ "horizon",
204
+ [
205
+ "keystone",
206
+ [
207
+ "glance",
208
+ "cinder",
209
+ ["neutron", ["octavia"]],
210
+ "designate",
211
+ ["placement", ["nova"]],
212
+ ],
213
+ ],
214
+ ],
202
215
  "collection-openstack": [
203
216
  "horizon",
204
217
  [
@@ -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/tasks/conductor.py CHANGED
@@ -2,12 +2,15 @@
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 utils
13
+ from osism.tasks import Config, netbox, openstack
11
14
 
12
15
  app = Celery("conductor")
13
16
  app.config_from_object(Config)
@@ -20,12 +23,6 @@ configuration = {}
20
23
  def celery_init_worker(**kwargs):
21
24
  global configuration
22
25
 
23
- # Parameters come from the environment, OS_*
24
- try:
25
- conn = openstack.connect()
26
- except keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions:
27
- pass
28
-
29
26
  with open("/etc/conductor.yml") as fp:
30
27
  configuration = yaml.load(fp, Loader=yaml.SafeLoader)
31
28
 
@@ -33,20 +30,20 @@ def celery_init_worker(**kwargs):
33
30
  logger.warning(
34
31
  "The conductor configuration is empty. That's probably wrong"
35
32
  )
33
+ configuration = {}
36
34
  return
37
35
 
38
36
  # Resolve all IDs in the conductor.yml
39
- if Config.enable_ironic in ["True", "true", "Yes", "yes"]:
37
+ if Config.enable_ironic.lower() in ["true", "yes"]:
40
38
  if "ironic_parameters" not in configuration:
41
39
  logger.error(
42
40
  "ironic_parameters not found in the conductor configuration"
43
41
  )
44
42
  return
45
43
 
46
- # TODO: use osism.tasks.openstack in the future
47
44
  if "driver_info" in configuration["ironic_parameters"]:
48
45
  if "deploy_kernel" in configuration["ironic_parameters"]["driver_info"]:
49
- result = conn.image.find_image(
46
+ result = openstack.image_get(
50
47
  configuration["ironic_parameters"]["driver_info"][
51
48
  "deploy_kernel"
52
49
  ]
@@ -59,7 +56,7 @@ def celery_init_worker(**kwargs):
59
56
  "deploy_ramdisk"
60
57
  in configuration["ironic_parameters"]["driver_info"]
61
58
  ):
62
- result = conn.image.find_image(
59
+ result = openstack.image_get(
63
60
  configuration["ironic_parameters"]["driver_info"][
64
61
  "deploy_ramdisk"
65
62
  ]
@@ -72,7 +69,7 @@ def celery_init_worker(**kwargs):
72
69
  "cleaning_network"
73
70
  in configuration["ironic_parameters"]["driver_info"]
74
71
  ):
75
- result = conn.network.find_network(
72
+ result = openstack.network_get(
76
73
  configuration["ironic_parameters"]["driver_info"][
77
74
  "cleaning_network"
78
75
  ]
@@ -85,7 +82,7 @@ def celery_init_worker(**kwargs):
85
82
  "provisioning_network"
86
83
  in configuration["ironic_parameters"]["driver_info"]
87
84
  ):
88
- result = conn.network.find_network(
85
+ result = openstack.network_get(
89
86
  configuration["ironic_parameters"]["driver_info"][
90
87
  "provisioning_network"
91
88
  ]
@@ -103,6 +100,320 @@ def setup_periodic_tasks(sender, **kwargs):
103
100
  @app.task(bind=True, name="osism.tasks.conductor.get_ironic_parameters")
104
101
  def get_ironic_parameters(self):
105
102
  if "ironic_parameters" in configuration:
106
- return configuration["ironic_parameters"]
103
+ # NOTE: Do not pass by reference, everybody gets their own copy to work with
104
+ return copy.deepcopy(configuration["ironic_parameters"])
107
105
 
108
106
  return {}
107
+
108
+
109
+ @app.task(bind=True, name="osism.tasks.conductor.sync_netbox_with_ironic")
110
+ def sync_netbox_with_ironic(self, force_update=False):
111
+ def deep_compare(a, b, updates):
112
+ """
113
+ Find items in a that do not exist in b or are different.
114
+ Write required changes into updates
115
+ """
116
+ for key, value in a.items():
117
+ if type(value) is not dict:
118
+ if key not in b or b[key] != value:
119
+ updates[key] = value
120
+ else:
121
+ updates[key] = {}
122
+ deep_compare(a[key], b[key], updates[key])
123
+ if not updates[key]:
124
+ updates.pop(key)
125
+
126
+ driver_params = {
127
+ "ipmi": {
128
+ "address": "ipmi_address",
129
+ "port": "ipmi_port",
130
+ "password": "ipmi_password",
131
+ },
132
+ "redfish": {
133
+ "address": "redfish_address",
134
+ "password": "redfish_password",
135
+ },
136
+ }
137
+
138
+ devices = list(netbox.get_devices_by_tags(["managed-by-ironic"]))
139
+
140
+ # NOTE: Find nodes in Ironic which are no longer present in netbox and remove them
141
+ device_names = [dev.name for dev in devices]
142
+ nodes = openstack.baremetal_node_list()
143
+ for node in nodes:
144
+ logger.info(f"Looking for {node['Name']} in netbox")
145
+ if node["Name"] not in device_names:
146
+ if (
147
+ not node["Instance UUID"]
148
+ and node["Provisioning State"] in ["enroll", "manageable", "available"]
149
+ and node["Power State"] in ["power off", None]
150
+ ):
151
+ logger.info(
152
+ f"Cleaning up baremetal node not found in netbox: {node['Name']}"
153
+ )
154
+ flavor_name = "osism-" + node["Name"]
155
+ flavor = openstack.compute_flavor_get(flavor_name)
156
+ if flavor:
157
+ logger.info(f"Deleting flavor {flavor_name}")
158
+ openstack.compute_flavor_delete(flavor)
159
+ for port in openstack.baremetal_port_list(
160
+ details=False, attributes=dict(node_uuid=node["UUID"])
161
+ ):
162
+ openstack.baremetal_port_delete(port.id)
163
+ openstack.baremetal_node_delete(node["UUID"])
164
+ else:
165
+ logger.error(
166
+ f"Cannot remove baremetal node because it is still provisioned or running: {node}"
167
+ )
168
+
169
+ # NOTE: Find nodes in netbox which are not present in Ironic and add them
170
+ for device in devices:
171
+ logger.info(f"Looking for {device.name} in ironic")
172
+
173
+ node_interfaces = list(netbox.get_interfaces_by_device(device.name))
174
+
175
+ node_attributes = get_ironic_parameters()
176
+ if (
177
+ "driver" in node_attributes
178
+ and node_attributes["driver"] in driver_params.keys()
179
+ ):
180
+ if "driver_info" in node_attributes:
181
+ address_key = driver_params[node_attributes["driver"]]["address"]
182
+ if address_key in node_attributes["driver_info"]:
183
+ if "oob_address" in device.custom_fields:
184
+ node_mgmt_address = device.custom_fields["oob_address"]
185
+ elif "address" in device.oob_ip:
186
+ node_mgmt_address = device.oob_ip["address"]
187
+ else:
188
+ node_mgmt_addresses = [
189
+ interface["address"]
190
+ for interface in node_interfaces
191
+ if interface.mgmt_only
192
+ and "address" in interface
193
+ and interface["address"]
194
+ ]
195
+ if len(node_mgmt_addresses) > 0:
196
+ node_mgmt_address = node_mgmt_addresses[0]
197
+ else:
198
+ node_mgmt_address = None
199
+ if node_mgmt_address:
200
+ node_attributes["driver_info"][address_key] = (
201
+ jinja2.Environment(loader=jinja2.BaseLoader())
202
+ .from_string(node_attributes["driver_info"][address_key])
203
+ .render(
204
+ remote_board_address=str(
205
+ ipaddress.ip_interface(node_mgmt_address).ip
206
+ )
207
+ )
208
+ )
209
+ else:
210
+ logger.error(f"Could not find out-of-band address for {device}")
211
+ node_attributes["driver_info"].pop(address_key, None)
212
+ if (
213
+ "port" in driver_params[node_attributes["driver"]]
214
+ and "oob_port" in device.custom_fields
215
+ and device.custom_fields["oob_port"]
216
+ ):
217
+ port_key = driver_params[node_attributes["driver"]]["port"]
218
+ node_attributes["driver_info"].update(
219
+ {port_key: device.custom_fields["oob_port"]}
220
+ )
221
+ node_attributes.update({"resource_class": device.name})
222
+ ports_attributes = [
223
+ dict(address=interface.mac_address)
224
+ for interface in node_interfaces
225
+ if interface.enabled and not interface.mgmt_only and interface.mac_address
226
+ ]
227
+ flavor_attributes = {
228
+ "ram": 1,
229
+ "disk": 0,
230
+ "vcpus": 1,
231
+ "is_public": False,
232
+ "extra_specs": {
233
+ "resources:CUSTOM_"
234
+ + device.name.upper().replace("-", "_").replace(".", "_"): "1",
235
+ "resources:VCPU": "0",
236
+ "resources:MEMORY_MB": "0",
237
+ "resources:DISK_GB": "0",
238
+ },
239
+ }
240
+
241
+ lock = Redlock(
242
+ key=f"lock_osism_tasks_conductor_sync_netbox_with_ironic-{device.name}",
243
+ masters={utils.redis},
244
+ auto_release_time=60,
245
+ )
246
+ if lock.acquire(timeout=20):
247
+ try:
248
+ logger.info(f"Processing device {device.name}")
249
+ node = openstack.baremetal_node_show(device.name, ignore_missing=True)
250
+ if not node:
251
+ logger.info(f"Creating baremetal node for {device.name}")
252
+ node = openstack.baremetal_node_create(device.name, node_attributes)
253
+ else:
254
+ # 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.
255
+ if (
256
+ device.custom_fields["provision_state"]
257
+ != node["provision_state"]
258
+ ):
259
+ netbox.set_provision_state(device.name, node["provision_state"])
260
+ if device.custom_fields["power_state"] != node["power_state"]:
261
+ netbox.set_power_state(device.name, node["power_state"])
262
+ # NOTE: Check whether the baremetal node needs to be updated
263
+ node_updates = {}
264
+ deep_compare(node_attributes, node, node_updates)
265
+ if "driver_info" in node_updates:
266
+ # 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
267
+ password_key = driver_params[node_attributes["driver"]][
268
+ "password"
269
+ ]
270
+ if password_key in node_updates["driver_info"]:
271
+ node_updates["driver_info"].pop(password_key, None)
272
+ if not node_updates["driver_info"]:
273
+ node_updates.pop("driver_info", None)
274
+ if node_updates or force_update:
275
+ logger.info(
276
+ f"Updating baremetal node for {device.name} with {node_updates}"
277
+ )
278
+ # 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
279
+ node = openstack.baremetal_node_update(
280
+ node["uuid"], node_attributes
281
+ )
282
+
283
+ node_ports = openstack.baremetal_port_list(
284
+ details=False, attributes=dict(node_uuid=node["uuid"])
285
+ )
286
+ # NOTE: Baremetal ports are only required for (i)pxe boot
287
+ if node["boot_interface"] in ["pxe", "ipxe"]:
288
+ for port_attributes in ports_attributes:
289
+ port_attributes.update({"node_id": node["uuid"]})
290
+ port = [
291
+ port
292
+ for port in node_ports
293
+ if port_attributes["address"].upper()
294
+ == port["address"].upper()
295
+ ]
296
+ if not port:
297
+ logger.info(
298
+ f"Creating baremetal port with MAC address {port_attributes['address']} for {device.name}"
299
+ )
300
+ openstack.baremetal_port_create(port_attributes)
301
+ else:
302
+ node_ports.remove(port[0])
303
+ for node_port in node_ports:
304
+ # NOTE: Delete remaining ports not found in netbox
305
+ logger.info(
306
+ f"Deleting baremetal port with MAC address {node_port['address']} for {device.name}"
307
+ )
308
+ openstack.baremetal_port_delete(node_port["id"])
309
+
310
+ node_validation = openstack.baremetal_node_validate(node["uuid"])
311
+ if node_validation["management"].result:
312
+ logger.info(
313
+ f"Validation of management interface successful for baremetal node for {device.name}"
314
+ )
315
+ if node["provision_state"] == "enroll":
316
+ logger.info(
317
+ f"Transitioning baremetal node to manageable state for {device.name}"
318
+ )
319
+ node = openstack.baremetal_node_set_provision_state(
320
+ node["uuid"], "manage"
321
+ )
322
+ node = openstack.baremetal_node_wait_for_nodes_provision_state(
323
+ node["uuid"], "manageable"
324
+ )
325
+ logger.info(f"Baremetal node for {device.name} is manageable")
326
+ if node_validation["boot"].result:
327
+ logger.info(
328
+ f"Validation of boot interface successful for baremetal node for {device.name}"
329
+ )
330
+ if node["provision_state"] == "manageable":
331
+ logger.info(
332
+ f"Transitioning baremetal node to available state for {device.name}"
333
+ )
334
+ node = openstack.baremetal_node_set_provision_state(
335
+ node["uuid"], "provide"
336
+ )
337
+ node = (
338
+ openstack.baremetal_node_wait_for_nodes_provision_state(
339
+ node["uuid"], "available"
340
+ )
341
+ )
342
+ logger.info(
343
+ f"Baremetal node for {device.name} is available"
344
+ )
345
+ else:
346
+ logger.info(
347
+ f"Validation of boot interface failed for baremetal node for {device.name}\nReason: {node_validation['boot'].reason}"
348
+ )
349
+ if node["provision_state"] == "available":
350
+ # NOTE: Demote node to manageable
351
+ logger.info(
352
+ f"Transitioning baremetal node to manageable state for {device.name}"
353
+ )
354
+ node = openstack.baremetal_node_set_provision_state(
355
+ node["uuid"], "manage"
356
+ )
357
+ node = (
358
+ openstack.baremetal_node_wait_for_nodes_provision_state(
359
+ node["uuid"], "manageable"
360
+ )
361
+ )
362
+ logger.info(
363
+ f"Baremetal node for {device.name} is manageable"
364
+ )
365
+ else:
366
+ logger.info(
367
+ f"Validation of management interface failed for baremetal node for {device.name}\nReason: {node_validation['management'].reason}"
368
+ )
369
+
370
+ flavor_name = "osism-" + device.name
371
+ flavor = openstack.compute_flavor_get(flavor_name)
372
+ if not flavor:
373
+ logger.info(f"Creating flavor for {flavor_name}")
374
+ flavor = openstack.compute_flavor_create(
375
+ flavor_name, flavor_attributes
376
+ )
377
+ else:
378
+ flavor_updates = {}
379
+ deep_compare(flavor_attributes, flavor, flavor_updates)
380
+ flavor_updates_extra_specs = flavor_updates.pop("extra_specs", None)
381
+ if flavor_updates:
382
+ logger.info(
383
+ f"Updating flavor for {device.name} with {flavor_updates}"
384
+ )
385
+ openstack.compute_flavor_delete(flavor)
386
+ flavor = openstack.compute_flavor_create(
387
+ flavor_name, flavor_attributes
388
+ )
389
+ elif flavor_updates_extra_specs:
390
+ logger.info(
391
+ f"Updating flavor extra_specs for {device.name} with {flavor_updates_extra_specs}"
392
+ )
393
+ openstack.compute_flavor_update_extra_specs(
394
+ flavor, flavor_updates_extra_specs
395
+ )
396
+ flavor = openstack.compute_flavor_get(flavor_name)
397
+ for extra_specs_key in flavor["extra_specs"].keys():
398
+ if (
399
+ extra_specs_key
400
+ not in flavor_attributes["extra_specs"].keys()
401
+ ):
402
+ logger.info(
403
+ f"Deleting flavor extra_specs property {extra_specs_key} for {device.name}"
404
+ )
405
+ flavor = (
406
+ openstack.compute_flavor_delete_extra_specs_property(
407
+ flavor, extra_specs_key
408
+ )
409
+ )
410
+
411
+ except Exception as exc:
412
+ logger.info(
413
+ f"Could not fully synchronize device {device.name} with ironic: {exc}"
414
+ )
415
+ finally:
416
+ lock.release()
417
+
418
+ else:
419
+ logger.error("Could not acquire lock for node {device.name}")