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 +2 -2
- osism/commands/netbox.py +17 -14
- osism/core/enums.py +14 -1
- osism/services/listener.py +28 -67
- osism/tasks/conductor.py +327 -16
- osism/tasks/netbox.py +84 -80
- osism/tasks/openstack.py +95 -155
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/METADATA +3 -3
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/RECORD +15 -17
- osism-0.20250407.0.dist-info/pbr.json +1 -0
- osism/actions/manage_device.py +0 -120
- osism/actions/manage_interface.py +0 -13
- osism-0.20250331.0.dist-info/pbr.json +0 -1
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/WHEEL +0 -0
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/entry_points.txt +0 -0
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/licenses/AUTHORS +0 -0
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250331.0.dist-info → osism-0.20250407.0.dist-info}/top_level.txt +0 -0
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.
|
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.
|
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,
|
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
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
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
|
[
|
osism/services/listener.py
CHANGED
@@ -11,8 +11,7 @@ from loguru import logger
|
|
11
11
|
import json
|
12
12
|
import requests
|
13
13
|
|
14
|
-
from osism import
|
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.
|
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.
|
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.
|
102
|
+
netbox.set_provision_state.delay(name, object_data["provision_state"])
|
107
103
|
|
108
|
-
def
|
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.
|
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
|
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(
|
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.
|
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.
|
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
|
-
|
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
|
5
|
+
import copy
|
6
|
+
import ipaddress
|
7
|
+
import jinja2
|
6
8
|
from loguru import logger
|
7
|
-
import
|
9
|
+
from pottery import Redlock
|
8
10
|
import yaml
|
9
11
|
|
10
|
-
from osism
|
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 ["
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
-
|
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}")
|