Browse Source

Add Redfish as OOB driver

This patch implements Refish as new OOB driver for Drydock.
All the existing Drydock Orchestrator actions are implemented.

Change-Id: I31d653fb41189a18c34cfafb0f490ca4f4d661b5
Hemanth Nakkina 9 months ago
parent
commit
da0c7e831e

+ 1
- 0
charts/drydock/values.yaml View File

@@ -325,6 +325,7 @@ conf:
325 325
       ingester:
326 326
         - 'drydock_provisioner.ingester.plugins.yaml.YamlIngester'
327 327
       oob_driver:
328
+        - 'drydock_provisioner.drivers.oob.redfish_driver.driver.RedfishDriver'
328 329
         - 'drydock_provisioner.drivers.oob.pyghmi_driver.driver.PyghmiDriver'
329 330
         - 'drydock_provisioner.drivers.oob.manual_driver.driver.ManualDriver'
330 331
         - 'drydock_provisioner.drivers.oob.libvirt_driver.driver.LibvirtDriver'

+ 19
- 0
etc/drydock/drydock.conf.sample View File

@@ -370,6 +370,25 @@
370 370
 #poll_interval = 10
371 371
 
372 372
 
373
+[redfish_driver]
374
+
375
+#
376
+# From drydock_provisioner
377
+#
378
+
379
+# Maximum number of connection retries to Redfish server
380
+#max_retries = 5
381
+
382
+# Maximum reties to wait for power state change
383
+#power_state_change_max_retries = 18
384
+
385
+# Polling interval in seconds between retries for power state change
386
+#power_state_change_retry_interval = 10
387
+
388
+# Use SSL to communicate with Redfish API server (boolean value)
389
+#use_ssl = true
390
+
391
+
373 392
 [timeouts]
374 393
 
375 394
 #

+ 1
- 1
python/drydock_provisioner/drivers/node/maasdriver/models/machine.py View File

@@ -569,7 +569,7 @@ class Machines(model_base.ResourceCollectionBase):
569 569
         """
570 570
         maas_node = None
571 571
 
572
-        if node_model.oob_type == 'ipmi':
572
+        if node_model.oob_type == 'ipmi' or node_model.oob_type == 'redfish':
573 573
             node_oob_network = node_model.oob_parameters['network']
574 574
             node_oob_ip = node_model.get_network_address(node_oob_network)
575 575
 

+ 0
- 0
python/drydock_provisioner/drivers/oob/redfish_driver/__init__.py View File


+ 0
- 0
python/drydock_provisioner/drivers/oob/redfish_driver/actions/__init__.py View File


+ 443
- 0
python/drydock_provisioner/drivers/oob/redfish_driver/actions/oob.py View File

@@ -0,0 +1,443 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Driver for controlling OOB interface via Redfish.
15
+
16
+Based on Redfish Rest API specification.
17
+"""
18
+
19
+import time
20
+
21
+from oslo_config import cfg
22
+
23
+from drydock_provisioner.orchestrator.actions.orchestrator import BaseAction
24
+from drydock_provisioner.drivers.oob.redfish_driver.client import RedfishException
25
+from drydock_provisioner.drivers.oob.redfish_driver.client import RedfishSession
26
+
27
+import drydock_provisioner.error as errors
28
+import drydock_provisioner.objects.fields as hd_fields
29
+
30
+class RedfishBaseAction(BaseAction):
31
+    """Base action for Redfish executed actions."""
32
+
33
+    def get_redfish_session(self, node):
34
+        """Initialize a Redfish session to the node.
35
+
36
+        :param node: instance of objects.BaremetalNode
37
+        :return: An instance of client.RedfishSession initialized to node's Redfish interface
38
+        """
39
+        if node.oob_type != 'redfish':
40
+            raise errors.DriverError("Node OOB type is not Redfish")
41
+
42
+        oob_network = node.oob_parameters['network']
43
+        oob_address = node.get_network_address(oob_network)
44
+        if oob_address is None:
45
+            raise errors.DriverError(
46
+                "Node %s has no OOB Redfish address" % (node.name))
47
+
48
+        oob_account = node.oob_parameters['account']
49
+        oob_credential = node.oob_parameters['credential']
50
+
51
+        self.logger.debug("Starting Redfish session to %s with %s" %
52
+                          (oob_address, oob_account))
53
+        try:
54
+            redfish_obj = RedfishSession(host=oob_address,
55
+                                         account=oob_account,
56
+                                         password=oob_credential,
57
+                                         use_ssl=cfg.CONF.redfish_driver.use_ssl,
58
+                                         connection_retries=cfg.CONF.redfish_driver.max_retries)
59
+        except (RedfishException, errors.DriverError) as iex:
60
+            self.logger.error(
61
+                "Error initializing Redfish session for node %s" % node.name)
62
+            self.logger.error("Redfish Exception: %s" % str(iex))
63
+            redfish_obj = None
64
+
65
+        return redfish_obj
66
+
67
+    def exec_redfish_command(self, node, session, func, *args):
68
+        """Call a Redfish command after establishing a session.
69
+
70
+        :param node: Instance of objects.BaremetalNode to execute against
71
+        :param session: Redfish session
72
+        :param func: The redfish Command method to call
73
+        :param args: The args to pass the func
74
+        """
75
+        try:
76
+            self.logger.debug("Calling Redfish command %s on %s" %
77
+                              (func.__name__, node.name))
78
+            response = func(session, *args)
79
+            return response
80
+        except RedfishException as iex:
81
+            self.logger.error(
82
+                "Error executing Redfish command %s for node %s" % (func.__name__, node.name))
83
+            self.logger.error("Redfish Exception: %s" % str(iex))
84
+
85
+        raise errors.DriverError("Redfish command failed.")
86
+
87
+
88
+class ValidateOobServices(RedfishBaseAction):
89
+    """Action to validate OOB services are available."""
90
+
91
+    def start(self):
92
+        self.task.add_status_msg(
93
+            msg="OOB does not require services.",
94
+            error=False,
95
+            ctx='NA',
96
+            ctx_type='NA')
97
+        self.task.set_status(hd_fields.TaskStatus.Complete)
98
+        self.task.success()
99
+        self.task.save()
100
+
101
+        return
102
+
103
+
104
+class ConfigNodePxe(RedfishBaseAction):
105
+    """Action to configure PXE booting via OOB."""
106
+
107
+    def start(self):
108
+        self.task.set_status(hd_fields.TaskStatus.Running)
109
+        self.task.save()
110
+
111
+        node_list = self.orchestrator.get_target_nodes(self.task)
112
+
113
+        for n in node_list:
114
+            self.task.add_status_msg(
115
+                msg="Redfish doesn't configure PXE options.",
116
+                error=True,
117
+                ctx=n.name,
118
+                ctx_type='node')
119
+        self.task.set_status(hd_fields.TaskStatus.Complete)
120
+        self.task.failure()
121
+        self.task.save()
122
+        return
123
+
124
+
125
+class SetNodeBoot(RedfishBaseAction):
126
+    """Action to configure a node to PXE boot."""
127
+
128
+    def start(self):
129
+        self.task.set_status(hd_fields.TaskStatus.Running)
130
+        self.task.save()
131
+
132
+        node_list = self.orchestrator.get_target_nodes(self.task)
133
+
134
+        for n in node_list:
135
+            self.logger.debug("Setting bootdev to PXE for %s" % n.name)
136
+            self.task.add_status_msg(
137
+                msg="Setting node to PXE boot.",
138
+                error=False,
139
+                ctx=n.name,
140
+                ctx_type='node')
141
+
142
+            bootdev = None
143
+            try:
144
+                session = self.get_redfish_session(n)
145
+                bootdev = self.exec_redfish_command(n, session, RedfishSession.get_bootdev)
146
+                if bootdev.get('bootdev', '') != 'Pxe':
147
+                    self.exec_redfish_command(n, session, RedfishSession.set_bootdev, 'Pxe')
148
+                    bootdev = self.exec_redfish_command(n, session, RedfishSession.get_bootdev)
149
+                session.close_session()
150
+            except errors.DriverError:
151
+                pass
152
+
153
+            if bootdev is not None and (bootdev.get('bootdev',
154
+                                                    '') == 'Pxe'):
155
+                self.task.add_status_msg(
156
+                    msg="Set bootdev to PXE.",
157
+                    error=False,
158
+                    ctx=n.name,
159
+                    ctx_type='node')
160
+                self.logger.debug("%s reports bootdev of network" % n.name)
161
+                self.task.success(focus=n.name)
162
+            else:
163
+                self.task.add_status_msg(
164
+                    msg="Unable to set bootdev to PXE.",
165
+                    error=True,
166
+                    ctx=n.name,
167
+                    ctx_type='node')
168
+                self.task.failure(focus=n.name)
169
+                self.logger.warning(
170
+                    "Unable to set node %s to PXE boot." % (n.name))
171
+
172
+        self.task.set_status(hd_fields.TaskStatus.Complete)
173
+        self.task.save()
174
+        return
175
+
176
+
177
+class PowerOffNode(RedfishBaseAction):
178
+    """Action to power off a node via Redfish."""
179
+
180
+    def start(self):
181
+        self.task.set_status(hd_fields.TaskStatus.Running)
182
+        self.task.save()
183
+
184
+        node_list = self.orchestrator.get_target_nodes(self.task)
185
+
186
+        for n in node_list:
187
+            self.logger.debug("Sending set_power = off command to %s" % n.name)
188
+            self.task.add_status_msg(
189
+                msg="Sending set_power = off command.",
190
+                error=False,
191
+                ctx=n.name,
192
+                ctx_type='node')
193
+            session = self.get_redfish_session(n)
194
+
195
+            # If power is already off, continue with the next node
196
+            power_state = self.exec_redfish_command(n, RedfishSession.get_power)
197
+            if power_state is not None and (power_state.get(
198
+                    'powerstate', '') == 'Off'):
199
+                self.task.add_status_msg(
200
+                    msg="Node reports power off.",
201
+                    error=False,
202
+                    ctx=n.name,
203
+                    ctx_type='node')
204
+                self.logger.debug(
205
+                    "Node %s reports powerstate already off. No action required" % n.name)
206
+                self.task.success(focus=n.name)
207
+                continue
208
+
209
+            self.exec_redfish_command(n, session, RedfishSession.set_power, 'ForceOff')
210
+
211
+            attempts = cfg.CONF.redfish_driver.power_state_change_max_retries
212
+
213
+            while attempts > 0:
214
+                self.logger.debug("Polling powerstate waiting for success.")
215
+                power_state = self.exec_redfish_command(n, RedfishSession.get_power)
216
+                if power_state is not None and (power_state.get(
217
+                        'powerstate', '') == 'Off'):
218
+                    self.task.add_status_msg(
219
+                        msg="Node reports power off.",
220
+                        error=False,
221
+                        ctx=n.name,
222
+                        ctx_type='node')
223
+                    self.logger.debug(
224
+                        "Node %s reports powerstate of off" % n.name)
225
+                    self.task.success(focus=n.name)
226
+                    break
227
+                time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval)
228
+                attempts = attempts - 1
229
+
230
+            if power_state is not None and (power_state.get('powerstate', '')
231
+                                            != 'Off'):
232
+                self.task.add_status_msg(
233
+                    msg="Node failed to power off.",
234
+                    error=True,
235
+                    ctx=n.name,
236
+                    ctx_type='node')
237
+                self.logger.error("Giving up on Redfish command to %s" % n.name)
238
+                self.task.failure(focus=n.name)
239
+
240
+            session.close_session()
241
+
242
+        self.task.set_status(hd_fields.TaskStatus.Complete)
243
+        self.task.save()
244
+        return
245
+
246
+
247
+class PowerOnNode(RedfishBaseAction):
248
+    """Action to power on a node via Redfish."""
249
+
250
+    def start(self):
251
+        self.task.set_status(hd_fields.TaskStatus.Running)
252
+        self.task.save()
253
+
254
+        node_list = self.orchestrator.get_target_nodes(self.task)
255
+
256
+        for n in node_list:
257
+            self.logger.debug("Sending set_power = on command to %s" % n.name)
258
+            self.task.add_status_msg(
259
+                msg="Sending set_power = on command.",
260
+                error=False,
261
+                ctx=n.name,
262
+                ctx_type='node')
263
+            session = self.get_redfish_session(n)
264
+
265
+            # If power is already on, continue with the next node
266
+            power_state = self.exec_redfish_command(n, RedfishSession.get_power)
267
+            if power_state is not None and (power_state.get(
268
+                    'powerstate', '') == 'On'):
269
+                self.task.add_status_msg(
270
+                    msg="Node reports power on.",
271
+                    error=False,
272
+                    ctx=n.name,
273
+                    ctx_type='node')
274
+                self.logger.debug(
275
+                    "Node %s reports powerstate already on. No action required" % n.name)
276
+                self.task.success(focus=n.name)
277
+                continue
278
+
279
+            self.exec_redfish_command(n, session, RedfishSession.set_power, 'On')
280
+
281
+            attempts = cfg.CONF.redfish_driver.power_state_change_max_retries
282
+
283
+            while attempts > 0:
284
+                self.logger.debug("Polling powerstate waiting for success.")
285
+                power_state = self.exec_redfish_command(n, session, RedfishSession.get_power)
286
+                if power_state is not None and (power_state.get(
287
+                        'powerstate', '') == 'On'):
288
+                    self.logger.debug(
289
+                        "Node %s reports powerstate of on" % n.name)
290
+                    self.task.add_status_msg(
291
+                        msg="Node reports power on.",
292
+                        error=False,
293
+                        ctx=n.name,
294
+                        ctx_type='node')
295
+                    self.task.success(focus=n.name)
296
+                    break
297
+                time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval)
298
+                attempts = attempts - 1
299
+
300
+            if power_state is not None and (power_state.get('powerstate', '')
301
+                                            != 'On'):
302
+                self.task.add_status_msg(
303
+                    msg="Node failed to power on.",
304
+                    error=True,
305
+                    ctx=n.name,
306
+                    ctx_type='node')
307
+                self.logger.error("Giving up on Redfish command to %s" % n.name)
308
+                self.task.failure(focus=n.name)
309
+
310
+            session.close_session()
311
+
312
+        self.task.set_status(hd_fields.TaskStatus.Complete)
313
+        self.task.save()
314
+        return
315
+
316
+
317
+class PowerCycleNode(RedfishBaseAction):
318
+    """Action to hard powercycle a node via Redfish."""
319
+
320
+    def start(self):
321
+        self.task.set_status(hd_fields.TaskStatus.Running)
322
+        self.task.save()
323
+
324
+        node_list = self.orchestrator.get_target_nodes(self.task)
325
+
326
+        for n in node_list:
327
+            self.logger.debug("Sending set_power = off command to %s" % n.name)
328
+            self.task.add_status_msg(
329
+                msg="Power cycling node via Redfish.",
330
+                error=False,
331
+                ctx=n.name,
332
+                ctx_type='node')
333
+            session = self.get_redfish_session(n)
334
+            self.exec_redfish_command(n, session, RedfishSession.set_power, 'ForceOff')
335
+
336
+            # Wait for power state of off before booting back up
337
+            attempts = cfg.CONF.redfish_driver.power_state_change_max_retries
338
+
339
+            while attempts > 0:
340
+                power_state = self.exec_redfish_command(n, session, RedfishSession.get_power)
341
+                if power_state is not None and power_state.get(
342
+                        'powerstate', '') == 'Off':
343
+                    self.logger.debug("%s reports powerstate of off" % n.name)
344
+                    break
345
+                elif power_state is None:
346
+                    self.logger.debug(
347
+                        "No response on Redfish power query to %s" % n.name)
348
+                time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval)
349
+                attempts = attempts - 1
350
+
351
+            if power_state.get('powerstate', '') != 'Off':
352
+                self.task.add_status_msg(
353
+                    msg="Failed to power down during power cycle.",
354
+                    error=True,
355
+                    ctx=n.name,
356
+                    ctx_type='node')
357
+                self.logger.warning(
358
+                    "Failed powering down node %s during power cycle task" %
359
+                    n.name)
360
+                self.task.failure(focus=n.name)
361
+                break
362
+
363
+            self.logger.debug("Sending set_power = on command to %s" % n.name)
364
+            self.exec_redfish_command(n, session, RedfishSession.set_power, 'On')
365
+
366
+            attempts = cfg.CONF.redfish_driver.power_state_change_max_retries
367
+
368
+            while attempts > 0:
369
+                power_state = self.exec_redfish_command(n, session, RedfishSession.get_power)
370
+                if power_state is not None and power_state.get(
371
+                        'powerstate', '') == 'On':
372
+                    self.logger.debug("%s reports powerstate of on" % n.name)
373
+                    break
374
+                elif power_state is None:
375
+                    self.logger.debug(
376
+                        "No response on Redfish power query to %s" % n.name)
377
+                time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval)
378
+                attempts = attempts - 1
379
+
380
+            if power_state is not None and (power_state.get('powerstate',
381
+                                                            '') == 'On'):
382
+                self.task.add_status_msg(
383
+                    msg="Node power cycle complete.",
384
+                    error=False,
385
+                    ctx=n.name,
386
+                    ctx_type='node')
387
+                self.task.success(focus=n.name)
388
+            else:
389
+                self.task.add_status_msg(
390
+                    msg="Failed to power up during power cycle.",
391
+                    error=True,
392
+                    ctx=n.name,
393
+                    ctx_type='node')
394
+                self.logger.warning(
395
+                    "Failed powering up node %s during power cycle task" %
396
+                    n.name)
397
+                self.task.failure(focus=n.name)
398
+
399
+            session.close_session()
400
+
401
+        self.task.set_status(hd_fields.TaskStatus.Complete)
402
+        self.task.save()
403
+        return
404
+
405
+
406
+class InterrogateOob(RedfishBaseAction):
407
+    """Action to complete a basic interrogation of the node Redfish interface."""
408
+
409
+    def start(self):
410
+        self.task.set_status(hd_fields.TaskStatus.Running)
411
+        self.task.save()
412
+
413
+        node_list = self.orchestrator.get_target_nodes(self.task)
414
+
415
+        for n in node_list:
416
+            try:
417
+                self.logger.debug(
418
+                    "Interrogating node %s Redfish interface." % n.name)
419
+                session = self.get_redfish_session(n)
420
+                powerstate = self.exec_redfish_command(n, session, RedfishSession.get_power)
421
+                session.close_session()
422
+                if powerstate is None:
423
+                    raise errors.DriverError()
424
+                self.task.add_status_msg(
425
+                    msg="Redfish interface interrogation yielded powerstate %s" %
426
+                    powerstate.get('powerstate'),
427
+                    error=False,
428
+                    ctx=n.name,
429
+                    ctx_type='node')
430
+                self.task.success(focus=n.name)
431
+            except errors.DriverError:
432
+                self.logger.debug(
433
+                    "Interrogating node %s Redfish interface failed." % n.name)
434
+                self.task.add_status_msg(
435
+                    msg="Redfish interface interrogation failed.",
436
+                    error=True,
437
+                    ctx=n.name,
438
+                    ctx_type='node')
439
+                self.task.failure(focus=n.name)
440
+
441
+        self.task.set_status(hd_fields.TaskStatus.Complete)
442
+        self.task.save()
443
+        return

+ 177
- 0
python/drydock_provisioner/drivers/oob/redfish_driver/client.py View File

@@ -0,0 +1,177 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Redfish object to provide commands.
15
+
16
+Uses Redfish client to communicate to node.
17
+"""
18
+
19
+from redfish import AuthMethod, redfish_client
20
+from redfish.rest.v1 import ServerDownOrUnreachableError
21
+from redfish.rest.v1 import InvalidCredentialsError
22
+from redfish.rest.v1 import RetriesExhaustedError
23
+
24
+class RedfishSession(object):
25
+    """Redfish Client to provide OOB commands"""
26
+
27
+    def __init__(self, host, account, password, use_ssl=True, connection_retries=10):
28
+        try:
29
+            if use_ssl:
30
+                redfish_url = 'https://' + host
31
+            else:
32
+                redfish_url = 'http://' + host
33
+            self.redfish_client = redfish_client(base_url=redfish_url,
34
+                                                 username=account,
35
+                                                 password=password)
36
+
37
+            self.redfish_client.MAX_RETRY = connection_retries
38
+            self.redfish_client.login(auth=AuthMethod.SESSION)
39
+        except RetriesExhaustedError:
40
+            raise RedfishException("Login failed: Retries exhausted")
41
+        except InvalidCredentialsError:
42
+            raise RedfishException("Login failed: Invalid credentials")
43
+        except ServerDownOrUnreachableError:
44
+            raise RedfishException("Login failed: Server unreachable")
45
+
46
+    def __del__(self):
47
+        self.redfish_client.logout()
48
+
49
+    def close_session(self):
50
+        self.redfish_client.logout()
51
+
52
+    def get_system_instance(self):
53
+        response = self.redfish_client.get("/redfish/v1/Systems")
54
+
55
+        if response.status != 200:
56
+            raise RedfishException(response._read)
57
+
58
+        # Assumption that only one system is available on Node
59
+        if response.dict["Members@odata.count"] != 1:
60
+            raise RedfishException("Number of systems are more than one in the node")
61
+        instance = response.dict["Members"][0]["@odata.id"]
62
+
63
+        return instance
64
+
65
+    def get_bootdev(self):
66
+        """Get current boot type information from Node.
67
+
68
+        :raises: RedfishException on an error
69
+        :return: dict -- response will return as dict in format of
70
+                 {'bootdev': bootdev}
71
+        """
72
+        instance = self.get_system_instance()
73
+        response = self.redfish_client.get(path=instance)
74
+
75
+        if response.status != 200:
76
+            raise RedfishException(response._read)
77
+
78
+        bootdev = response.dict["Boot"]["BootSourceOverrideTarget"]
79
+        return {'bootdev': bootdev}
80
+
81
+    def set_bootdev(self, bootdev, **kwargs):
82
+        """Set boot type on the Node for next boot.
83
+
84
+        :param bootdev: Boot source for the next boot
85
+                        * None - Boot from the normal boot device.
86
+                        * Pxe - Boot from network
87
+                        * Cd - Boot from CD/DVD disc
88
+                        * Usb - Boot from USB device specified by system BIOS
89
+                        * Hdd - Boot from Hard drive
90
+                        * BiosSetup - Boot to bios setup utility
91
+                        * Utilities - Boot manufacurer utlities program
92
+                        * UefiTarget - Boot to the UEFI Device specified in the
93
+                                       UefiTargetBootSourceOverride property
94
+                        * UefiShell - Boot to the UEFI Shell
95
+                        * UefiHttp - Boot from UEFI HTTP network location
96
+        :param **kwargs: To specify extra arguments for a given bootdev
97
+                         Example to specify UefiTargetBootSourceOverride value
98
+                         for bootdev UefiTarget
99
+        :raises: RedfishException on an error
100
+        :return: dict -- response will return as dict in format of
101
+                 {'bootdev': bootdev}
102
+        """
103
+        instance = self.get_system_instance()
104
+
105
+        payload = {
106
+            "Boot": {
107
+                "BootSourceOverrideEnabled": "Once",
108
+                "BootSourceOverrideTarget": bootdev,
109
+            }
110
+        }
111
+
112
+        if bootdev == 'UefiTarget':
113
+            payload['Boot']['UefiTargetBootSourceOverride'] = kwargs.get(
114
+                'UefiTargetBootSourceOverride', '')
115
+
116
+        response = self.redfish_client.patch(path=instance, body=payload)
117
+        if response.status != 200:
118
+            raise RedfishException(response._read)
119
+
120
+        return {'bootdev': bootdev}
121
+
122
+    def get_power(self):
123
+        """Get current power state information from Node.
124
+
125
+        :raises: RedfishException on an error
126
+        :return: dict -- response will return as dict in format of
127
+                 {'powerstate': powerstate}
128
+        """
129
+        instance = self.get_system_instance()
130
+
131
+        response = self.redfish_client.get(path=instance)
132
+        if response.status != 200:
133
+            raise RedfishException(response._read)
134
+
135
+        powerstate = response.dict["PowerState"]
136
+        return {'powerstate': powerstate}
137
+
138
+    def set_power(self, powerstate):
139
+        """Request power change on the node.
140
+
141
+        :param powerstate: set power change
142
+                           * On - Power On the unit
143
+                           * ForceOff - Turn off immediately (non graceful)
144
+                           * PushPowerButton - Simulate pressing physical
145
+                                               power button
146
+                           * GracefulRestart - Perform a graceful shutdown
147
+                                               and then start
148
+
149
+        :raises: RedfishException on an error
150
+        :return: dict -- response will return as dict in format of
151
+                 {'powerstate': powerstate}
152
+        """
153
+        instance = self.get_system_instance()
154
+
155
+        if powerstate not in ["On", "ForceOff", "PushPowerButton", "GracefulRestart"]:
156
+            raise RedfishException("Unsupported powerstate")
157
+
158
+        current_state = self.get_power()
159
+        if (powerstate == "On" and current_state["powerstate"] == "On") or \
160
+           (powerstate == "ForceOff" and current_state["powerstate"] == "Off"):
161
+            return {'powerstate': powerstate}
162
+
163
+        payload = {
164
+            "ResetType": powerstate
165
+        }
166
+
167
+        url = instance + "/Actions/ComputerSystem.Reset"
168
+        response = self.redfish_client.post(path=url, body=payload)
169
+        if response.status in [200, 201, 204]:
170
+            return {'powerstate': powerstate}
171
+        else:
172
+            raise RedfishException(response._read)
173
+
174
+
175
+class RedfishException(Exception):
176
+    """Redfish Exception with error in message"""
177
+    pass

+ 171
- 0
python/drydock_provisioner/drivers/oob/redfish_driver/driver.py View File

@@ -0,0 +1,171 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Driver for controlling OOB interface via Redfish.
15
+
16
+Based on Redfish Rest API specification.
17
+"""
18
+
19
+import uuid
20
+import logging
21
+import concurrent.futures
22
+
23
+from oslo_config import cfg
24
+
25
+import drydock_provisioner.error as errors
26
+import drydock_provisioner.config as config
27
+
28
+import drydock_provisioner.objects.fields as hd_fields
29
+
30
+import drydock_provisioner.drivers.oob.driver as oob_driver
31
+import drydock_provisioner.drivers.driver as generic_driver
32
+
33
+from .actions.oob import ValidateOobServices
34
+from .actions.oob import ConfigNodePxe
35
+from .actions.oob import SetNodeBoot
36
+from .actions.oob import PowerOffNode
37
+from .actions.oob import PowerOnNode
38
+from .actions.oob import PowerCycleNode
39
+from .actions.oob import InterrogateOob
40
+
41
+
42
+class RedfishDriver(oob_driver.OobDriver):
43
+    """Driver for executing OOB actions via Redfish library."""
44
+
45
+    redfish_driver_options = [
46
+        cfg.IntOpt(
47
+            'max_retries',
48
+            default=10,
49
+            min=1,
50
+            help='Maximum number of connection retries to Redfish server'),
51
+        cfg.IntOpt(
52
+            'power_state_change_max_retries',
53
+            default=18,
54
+            min=1,
55
+            help='Maximum reties to wait for power state change'),
56
+        cfg.IntOpt(
57
+            'power_state_change_retry_interval',
58
+            default=10,
59
+            help='Polling interval in seconds between retries for power state change'),
60
+        cfg.BoolOpt(
61
+            'use_ssl',
62
+            default=True,
63
+            help='Use SSL to communicate with Redfish API server'),
64
+    ]
65
+
66
+    oob_types_supported = ['redfish']
67
+
68
+    driver_name = "redfish_driver"
69
+    driver_key = "redfish_driver"
70
+    driver_desc = "Redfish OOB Driver"
71
+
72
+    action_class_map = {
73
+        hd_fields.OrchestratorAction.ValidateOobServices: ValidateOobServices,
74
+        hd_fields.OrchestratorAction.ConfigNodePxe: ConfigNodePxe,
75
+        hd_fields.OrchestratorAction.SetNodeBoot: SetNodeBoot,
76
+        hd_fields.OrchestratorAction.PowerOffNode: PowerOffNode,
77
+        hd_fields.OrchestratorAction.PowerOnNode: PowerOnNode,
78
+        hd_fields.OrchestratorAction.PowerCycleNode: PowerCycleNode,
79
+        hd_fields.OrchestratorAction.InterrogateOob: InterrogateOob,
80
+    }
81
+
82
+    def __init__(self, **kwargs):
83
+        super().__init__(**kwargs)
84
+
85
+        cfg.CONF.register_opts(
86
+            RedfishDriver.redfish_driver_options, group=RedfishDriver.driver_key)
87
+
88
+        self.logger = logging.getLogger(
89
+            config.config_mgr.conf.logging.oobdriver_logger_name)
90
+
91
+    def execute_task(self, task_id):
92
+        task = self.state_manager.get_task(task_id)
93
+
94
+        if task is None:
95
+            self.logger.error("Invalid task %s" % (task_id))
96
+            raise errors.DriverError("Invalid task %s" % (task_id))
97
+
98
+        if task.action not in self.supported_actions:
99
+            self.logger.error("Driver %s doesn't support task action %s" %
100
+                              (self.driver_desc, task.action))
101
+            raise errors.DriverError("Driver %s doesn't support task action %s"
102
+                                     % (self.driver_desc, task.action))
103
+
104
+        task.set_status(hd_fields.TaskStatus.Running)
105
+        task.save()
106
+
107
+        target_nodes = self.orchestrator.get_target_nodes(task)
108
+
109
+        with concurrent.futures.ThreadPoolExecutor(max_workers=16) as e:
110
+            subtask_futures = dict()
111
+            for n in target_nodes:
112
+                sub_nf = self.orchestrator.create_nodefilter_from_nodelist([n])
113
+                subtask = self.orchestrator.create_task(
114
+                    action=task.action,
115
+                    design_ref=task.design_ref,
116
+                    node_filter=sub_nf)
117
+                task.register_subtask(subtask)
118
+                self.logger.debug(
119
+                    "Starting Redfish subtask %s for action %s on node %s" %
120
+                    (str(subtask.get_id()), task.action, n.name))
121
+
122
+                action_class = self.action_class_map.get(task.action, None)
123
+                if action_class is None:
124
+                    self.logger.error(
125
+                        "Could not find action resource for action %s" %
126
+                        task.action)
127
+                    self.task.failure()
128
+                    break
129
+                action = action_class(subtask, self.orchestrator,
130
+                                      self.state_manager)
131
+                subtask_futures[subtask.get_id().bytes] = e.submit(
132
+                    action.start)
133
+
134
+            timeout = config.config_mgr.conf.timeouts.drydock_timeout
135
+            finished, running = concurrent.futures.wait(
136
+                subtask_futures.values(), timeout=(timeout * 60))
137
+
138
+            for t, f in subtask_futures.items():
139
+                if not f.done():
140
+                    task.add_status_msg(
141
+                        msg="Subtask %s timed out before completing.",
142
+                        error=True,
143
+                        ctx=str(uuid.UUID(bytes=t)),
144
+                        ctx_type='task')
145
+                    task.failure()
146
+                else:
147
+                    if f.exception():
148
+                        self.logger.error(
149
+                            "Uncaught exception in subtask %s" % str(
150
+                                uuid.UUID(bytes=t)),
151
+                            exc_info=f.exception())
152
+            task.align_result()
153
+            task.bubble_results()
154
+            task.set_status(hd_fields.TaskStatus.Complete)
155
+            task.save()
156
+
157
+        return
158
+
159
+
160
+class RedfishActionRunner(generic_driver.DriverActionRunner):
161
+    """Threaded runner for a Redfish Action."""
162
+
163
+    def __init__(self, **kwargs):
164
+        super().__init__(**kwargs)
165
+
166
+        self.logger = logging.getLogger(
167
+            config.config_mgr.conf.logging.oobdriver_logger_name)
168
+
169
+
170
+def list_opts():
171
+    return {RedfishDriver.driver_key: RedfishDriver.redfish_driver_options}

+ 1
- 0
python/requirements-direct.txt View File

@@ -24,3 +24,4 @@ ulid2==0.1.1
24 24
 defusedxml===0.5.0
25 25
 libvirt-python==3.10.0
26 26
 beaker==1.9.1
27
+redfish==2.0.1

+ 1
- 0
python/requirements-lock.txt View File

@@ -61,6 +61,7 @@ python-keystoneclient==3.17.0
61 61
 python-mimeparse==1.6.0
62 62
 pytz==2018.5
63 63
 PyYAML==3.12
64
+redfish==2.0.1
64 65
 repoze.lru==0.7
65 66
 requests==2.19.1
66 67
 rfc3986==1.1.0

Loading…
Cancel
Save