1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
VirtualBox Driver Modules
"""
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
from ironic.common import boot_devices
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers import base
pyremotevbox = importutils.try_import('pyremotevbox')
if pyremotevbox:
from pyremotevbox import exception as virtualbox_exc
from pyremotevbox import vbox as virtualbox
IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING = {
boot_devices.PXE: 'Network',
boot_devices.DISK: 'HardDisk',
boot_devices.CDROM: 'DVD',
}
VIRTUALBOX_TO_IRONIC_DEVICE_MAPPING = {v: k
for k, v in IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING.items()}
VIRTUALBOX_TO_IRONIC_POWER_MAPPING = {
'PoweredOff': states.POWER_OFF,
'Running': states.POWER_ON,
'Error': states.ERROR
}
opts = [
cfg.IntOpt('port',
default=18083,
help='Port on which VirtualBox web service is listening.'),
]
CONF = cfg.CONF
CONF.register_opts(opts, group='virtualbox')
LOG = logging.getLogger(__name__)
REQUIRED_PROPERTIES = {
'virtualbox_vmname': _("Name of the VM in VirtualBox. Required."),
'virtualbox_host': _("IP address or hostname of the VirtualBox host. "
"Required.")
}
OPTIONAL_PROPERTIES = {
'virtualbox_username': _("Username for the VirtualBox host. "
"Default value is ''. Optional."),
'virtualbox_password': _("Password for 'virtualbox_username'. "
"Default value is ''. Optional."),
'virtualbox_port': _("Port on which VirtualBox web service is listening. "
"Optional."),
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
def _strip_virtualbox_from_param_name(param_name):
if param_name.startswith('virtualbox_'):
return param_name[11:]
else:
return param_name
def _parse_driver_info(node):
"""Gets the driver specific node driver info.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver.
:param node: an Ironic Node object.
:returns: a dict containing information from driver_info (or where
applicable, config values).
:raises: MissingParameterValue, if some required parameter(s) are missing
in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid value(s)
in the node's driver_info.
"""
info = node.driver_info
d_info = {}
missing_params = []
for param in REQUIRED_PROPERTIES:
try:
d_info_param_name = _strip_virtualbox_from_param_name(param)
d_info[d_info_param_name] = info[param]
except KeyError:
missing_params.append(param)
if missing_params:
msg = (_("The following parameters are missing in driver_info: %s") %
', '.join(missing_params))
raise exception.MissingParameterValue(msg)
for param in OPTIONAL_PROPERTIES:
if param in info:
d_info_param_name = _strip_virtualbox_from_param_name(param)
d_info[d_info_param_name] = info[param]
try:
d_info['port'] = int(d_info.get('port', CONF.virtualbox.port))
except ValueError:
msg = _("'virtualbox_port' is not an integer.")
raise exception.InvalidParameterValue(msg)
return d_info
def _run_virtualbox_method(node, ironic_method, vm_object_method,
*call_args, **call_kwargs):
"""Runs a method of pyremotevbox.vbox.VirtualMachine
This runs a method from pyremotevbox.vbox.VirtualMachine.
The VirtualMachine method to be invoked and the argument(s) to be
passed to it are to be provided.
:param node: an Ironic Node object.
:param ironic_method: the Ironic method which called
'_run_virtualbox_method'. This is used for logging only.
:param vm_object_method: The method on the VirtualMachine object
to be called.
:param call_args: The args to be passed to 'vm_object_method'.
:param call_kwargs: The kwargs to be passed to the 'vm_object_method'.
:returns: The value returned by 'vm_object_method'
:raises: VirtualBoxOperationFailed, if execution of 'vm_object_method'
failed.
:raises: InvalidParameterValue,
- if 'vm_object_method' is not a valid 'VirtualMachine' method.
- if some parameter(s) have invalid value(s) in the node's driver_info.
:raises: MissingParameterValue, if some required parameter(s) are missing
in the node's driver_info.
:raises: pyremotevbox.exception.VmInWrongPowerState, if operation cannot
be performed when vm is in the current power state.
"""
driver_info = _parse_driver_info(node)
try:
host = virtualbox.VirtualBoxHost(**driver_info)
vm_object = host.find_vm(driver_info['vmname'])
except virtualbox_exc.PyRemoteVBoxException as exc:
LOG.error(_LE("Failed while creating a VirtualMachine object for "
"node %(node_id)s. Error: %(error)s."),
{'node_id': node.uuid, 'error': exc})
raise exception.VirtualBoxOperationFailed(operation=vm_object_method,
error=exc)
try:
func = getattr(vm_object, vm_object_method)
except AttributeError:
error_msg = _("Invalid VirtualMachine method '%s' passed "
"to '_run_virtualbox_method'.")
raise exception.InvalidParameterValue(error_msg % vm_object_method)
try:
return func(*call_args, **call_kwargs)
except virtualbox_exc.PyRemoteVBoxException as exc:
error_msg = _LE("'%(ironic_method)s' failed for node %(node_id)s with "
"error: %(error)s.")
LOG.error(error_msg, {'ironic_method': ironic_method,
'node_id': node.uuid,
'error': exc})
raise exception.VirtualBoxOperationFailed(operation=vm_object_method,
error=exc)
class VirtualBoxPower(base.PowerInterface):
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check if node.driver_info contains the required credentials.
:param task: a TaskManager instance.
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info.
"""
_parse_driver_info(task.node)
def get_power_state(self, task):
"""Gets the current power state.
:param task: a TaskManager instance.
:returns: one of :mod:`ironic.common.states`
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info.
:raises: VirtualBoxOperationFailed, if error encountered from
VirtualBox operation.
"""
power_status = _run_virtualbox_method(task.node, 'get_power_state',
'get_power_status')
try:
return VIRTUALBOX_TO_IRONIC_POWER_MAPPING[power_status]
except KeyError:
msg = _LE("VirtualBox returned unknown state '%(state)s' for "
"node %(node)s")
LOG.error(msg, {'state': power_status, 'node': task.node.uuid})
return states.ERROR
@task_manager.require_exclusive_lock
def set_power_state(self, task, target_state):
"""Turn the current power state on or off.
:param task: a TaskManager instance.
:param target_state: The desired power state POWER_ON,POWER_OFF or
REBOOT from :mod:`ironic.common.states`.
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info OR if an invalid power state
was specified.
:raises: VirtualBoxOperationFailed, if error encountered from
VirtualBox operation.
"""
if target_state == states.POWER_OFF:
_run_virtualbox_method(task.node, 'set_power_state', 'stop')
elif target_state == states.POWER_ON:
_run_virtualbox_method(task.node, 'set_power_state', 'start')
elif target_state == states.REBOOT:
self.reboot(task)
else:
msg = _("'set_power_state' called with invalid power "
"state '%s'") % target_state
raise exception.InvalidParameterValue(msg)
@task_manager.require_exclusive_lock
def reboot(self, task):
"""Reboot the node.
:param task: a TaskManager instance.
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info.
:raises: VirtualBoxOperationFailed, if error encountered from
VirtualBox operation.
"""
_run_virtualbox_method(task.node, 'reboot', 'stop')
_run_virtualbox_method(task.node, 'reboot', 'start')
class VirtualBoxManagement(base.ManagementInterface):
def get_properties(self):
return COMMON_PROPERTIES
def validate(self, task):
"""Check that 'driver_info' contains required credentials.
Validates whether the 'driver_info' property of the supplied
task's node contains the required credentials information.
:param task: a task from TaskManager.
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info.
"""
_parse_driver_info(task.node)
def get_supported_boot_devices(self):
"""Get a list of the supported boot devices.
:returns: A list with the supported boot devices defined
in :mod:`ironic.common.boot_devices`.
"""
return list(IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING.keys())
def get_boot_device(self, task):
"""Get the current boot device for a node.
:param task: a task from TaskManager.
:returns: a dictionary containing:
'boot_device': one of the ironic.common.boot_devices or None
'persistent': True if boot device is persistent, False otherwise
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info.
:raises: VirtualBoxOperationFailed, if error encountered from
VirtualBox operation.
"""
boot_dev = _run_virtualbox_method(task.node, 'get_boot_device',
'get_boot_device')
persistent = True
ironic_boot_dev = VIRTUALBOX_TO_IRONIC_DEVICE_MAPPING.get(boot_dev,
None)
if not ironic_boot_dev:
persistent = None
msg = _LE("VirtualBox returned unknown boot device '%(device)s' "
"for node %(node)s")
LOG.error(msg, {'device': boot_dev, 'node': task.node.uuid})
return {'boot_device': ironic_boot_dev, 'persistent': persistent}
@task_manager.require_exclusive_lock
def set_boot_device(self, task, device, persistent=False):
"""Set the boot device for a node.
:param task: a task from TaskManager.
:param device: ironic.common.boot_devices
:param persistent: This argument is ignored as VirtualBox support only
persistent boot devices.
:raises: MissingParameterValue, if some required parameter(s) are
missing in the node's driver_info.
:raises: InvalidParameterValue, if some parameter(s) have invalid
value(s) in the node's driver_info.
:raises: VirtualBoxOperationFailed, if error encountered from
VirtualBox operation.
"""
# NOTE(rameshg87): VirtualBox has only persistent boot devices.
try:
boot_dev = IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING[device]
except KeyError:
raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % device)
try:
_run_virtualbox_method(task.node, 'set_boot_device',
'set_boot_device', boot_dev)
except virtualbox_exc.VmInWrongPowerState as exc:
# NOTE(rameshg87): We cannot change the boot device when the vm
# is powered on. This is a VirtualBox limitation. We just log
# the error silently and return because throwing error will cause
# deploys to fail (pxe and agent deploy mechanisms change the boot
# device after completing the deployment, when node is powered on).
# Since this is driver that is meant only for developers, this
# should be okay. Developers will need to set the boot device
# manually after powering off the vm when deployment is complete.
# This will be documented.
LOG.error(_LE("'set_boot_device' failed for node %(node_id)s "
"with error: %(error)s"),
{'node_id': task.node.uuid, 'error': exc})
def get_sensors_data(self, task):
"""Get sensors data.
:param task: a TaskManager instance.
:raises: FailedToGetSensorData when getting the sensor data fails.
:raises: FailedToParseSensorData when parsing sensor data fails.
:returns: returns a consistent format dict of sensor data grouped by
sensor type, which can be processed by Ceilometer.
"""
raise NotImplementedError()
|