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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
21 from ironic_lib import metrics_utils
22 from ironic_lib import utils as ironic_utils
23 from oslo_log import log as logging
24 from oslo_utils import importutils
26 from ironic.common import boot_devices
27 from ironic.common import exception
28 from ironic.common.glance_service import service_utils
29 from ironic.common.i18n import _, _translators
30 from ironic.common import images
31 from ironic.common import states
32 from ironic.common import utils
33 from ironic.conductor import utils as manager_utils
34 from ironic_virtmedia_driver.conf import CONF
35 from ironic.drivers import base
36 from ironic.drivers.modules import deploy_utils
37 from ironic_virtmedia_driver import virtmedia_exception
39 LOG = logging.getLogger(__name__)
41 METRICS = metrics_utils.get_metrics_logger(__name__)
43 REQUIRED_PROPERTIES = {
44 'virtmedia_deploy_iso': _("Deployment ISO image file name. "
48 COMMON_PROPERTIES = REQUIRED_PROPERTIES
51 def _parse_config_option():
52 """Parse config file options.
54 This method checks config file options validity.
56 :raises: InvalidParameterValue, if config option has invalid value.
59 if not os.path.isdir(CONF.remote_image_share_root):
61 _("Value '%s' for remote_image_share_root isn't a directory "
62 "or doesn't exist.") %
63 CONF.remote_image_share_root)
65 msg = (_("The following errors were encountered while parsing "
66 "config file:%s") % error_msgs)
67 raise exception.InvalidParameterValue(msg)
70 def _parse_driver_info(node):
71 """Gets the driver specific Node deployment info.
73 This method validates whether the 'driver_info' property of the
74 supplied node contains the required or optional information properly
75 for this driver to deploy images to the node.
77 :param node: a target node of the deployment
78 :returns: the driver_info values of the node.
79 :raises: MissingParameterValue, if any of the required parameters are
81 :raises: InvalidParameterValue, if any of the parameters have invalid
84 d_info = node.driver_info
87 deploy_info['virtmedia_deploy_iso'] = d_info.get('virtmedia_deploy_iso')
88 error_msg = _("Error validating virtual media deploy. Some parameters"
89 " were missing in node's driver_info")
90 deploy_utils.check_for_missing_params(deploy_info, error_msg)
92 if service_utils.is_image_href_ordinary_file_name(
93 deploy_info['virtmedia_deploy_iso']):
94 deploy_iso = os.path.join(CONF.remote_image_share_root,
95 deploy_info['virtmedia_deploy_iso'])
96 if not os.path.isfile(deploy_iso):
97 msg = (_("Deploy ISO file, %(deploy_iso)s, "
98 "not found for node: %(node)s.") %
99 {'deploy_iso': deploy_iso, 'node': node.uuid})
100 raise exception.InvalidParameterValue(msg)
104 def _parse_deploy_info(node):
105 """Gets the instance and driver specific Node deployment info.
107 This method validates whether the 'instance_info' and 'driver_info'
108 property of the supplied node contains the required information for
109 this driver to deploy images to the node.
111 :param node: a target node of the deployment
112 :returns: a dict with the instance_info and driver_info values.
113 :raises: MissingParameterValue, if any of the required parameters are
115 :raises: InvalidParameterValue, if any of the parameters have invalid
119 deploy_info.update(deploy_utils.get_image_instance_info(node))
120 deploy_info.update(_parse_driver_info(node))
124 def _get_deploy_iso_name(node):
125 """Returns the deploy ISO file name for a given node.
127 :param node: the node for which ISO file name is to be provided.
129 return "deploy-%s.iso" % node.name
131 def _get_boot_iso_name(node):
132 """Returns the boot ISO file name for a given node.
134 :param node: the node for which ISO file name is to be provided.
136 return "boot-%s.iso" % node.uuid
138 def _get_floppy_image_name(node):
139 """Returns the floppy image name for a given node.
141 :param node: the node for which image name is to be provided.
143 return "image-%s.img" % node.name
146 def _prepare_floppy_image(task, params):
147 """Prepares the floppy image for passing the parameters.
149 This method prepares a temporary vfat filesystem image, which
150 contains the parameters to be passed to the ramdisk.
151 Then it uploads the file NFS or CIFS server.
153 :param task: a TaskManager instance containing the node to act on.
154 :param params: a dictionary containing 'parameter name'->'value' mapping
155 to be passed to the deploy ramdisk via the floppy image.
156 :returns: floppy image filename
157 :raises: ImageCreationFailed, if it failed while creating the floppy image.
158 :raises: VirtmediaOperationError, if copying floppy image file failed.
160 floppy_filename = _get_floppy_image_name(task.node)
161 floppy_fullpathname = os.path.join(
162 CONF.remote_image_share_root, floppy_filename)
164 with tempfile.NamedTemporaryFile() as vfat_image_tmpfile_obj:
165 images.create_vfat_image(vfat_image_tmpfile_obj.name,
168 shutil.copyfile(vfat_image_tmpfile_obj.name,
171 operation = _("Copying floppy image file")
172 raise virtmedia_exception.VirtmediaOperationError(
173 operation=operation, error=e)
175 return floppy_filename
177 def _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename):
178 """ Quanta HW cannot attach 2 Virtual media at the moment.
179 Preparing CD which has floppy content at the end of it as
182 boot_iso_full_path = CONF.remote_image_share_root + bootable_iso_filename
183 floppy_image_full_path = CONF.remote_image_share_root + floppy_image_filename
184 tar_file_path = CONF.remote_image_share_root + floppy_image_filename + '.tar.gz'
186 # Prepare a temporary Tar file
187 tar = tarfile.open(tar_file_path, "w:gz")
188 tar.add(floppy_image_full_path, arcname=os.path.basename(floppy_image_full_path))
191 # Using dd append Tar to iso and remove Tar file
192 ironic_utils.dd(tar_file_path, boot_iso_full_path, 'bs=64k', 'conv=notrunc,sync', 'oflag=append')
194 os.remove(tar_file_path)
196 def _remove_share_file(share_filename):
197 """Remove given file from the share file system.
199 :param share_filename: a file name to be removed.
201 share_fullpathname = os.path.join(
202 CONF.remote_image_share_root, share_filename)
203 LOG.debug(_translators.log_info("_remove_share_file: Unlinking %s"), share_fullpathname)
204 ironic_utils.unlink_without_raise(share_fullpathname)
206 class VirtmediaBoot(base.BootInterface):
207 """Implementation of a boot interface using Virtual Media."""
210 """Constructor of VirtualMediaBoot.
212 :raises: InvalidParameterValue, if config option has invalid value.
214 super(VirtmediaBoot, self).__init__()
216 def get_properties(self):
217 return COMMON_PROPERTIES
219 @METRICS.timer('VirtualMediaBoot.validate')
220 def validate(self, task):
221 """Validate the deployment information for the task's node.
223 :param task: a TaskManager instance containing the node to act on.
224 :raises: InvalidParameterValue, if config option has invalid value.
225 :raises: InvalidParameterValue, if some information is invalid.
226 :raises: MissingParameterValue if 'kernel_id' and 'ramdisk_id' are
227 missing in the Glance image, or if 'kernel' and 'ramdisk' are
228 missing in the Non Glance image.
230 d_info = _parse_deploy_info(task.node)
231 if task.node.driver_internal_info.get('is_whole_disk_image'):
233 elif service_utils.is_glance_image(d_info['image_source']):
234 props = ['kernel_id', 'ramdisk_id']
236 props = ['kernel', 'ramdisk']
237 deploy_utils.validate_image_properties(task.context, d_info,
240 @METRICS.timer('VirtualMediaBoot.prepare_ramdisk')
241 def prepare_ramdisk(self, task, ramdisk_params):
242 """Prepares the deploy ramdisk using virtual media.
244 Prepares the options for the deployment ramdisk, sets the node to boot
245 from virtual media cdrom.
247 :param task: a TaskManager instance containing the node to act on.
248 :param ramdisk_params: the options to be passed to the deploy ramdisk.
249 :raises: ImageRefValidationFailed if no image service can handle
251 :raises: ImageCreationFailed, if it failed while creating the floppy
253 :raises: InvalidParameterValue if the validation of the
254 PowerInterface or ManagementInterface fails.
255 :raises: VirtmediaOperationError, if some operation fails.
258 # NOTE(TheJulia): If this method is being called by something
259 # aside from deployment and clean, such as conductor takeover, we
260 # should treat this as a no-op and move on otherwise we would modify
261 # the state of the node due to virtual media operations.
263 if (task.node.provision_state != states.DEPLOYING and
264 task.node.provision_state != states.CLEANING):
267 deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task)
268 ramdisk_params['BOOTIF'] = deploy_nic_mac
269 os_net_config = task.node.driver_info.get('os_net_config')
271 ramdisk_params['os_net_config'] = os_net_config
273 self._setup_deploy_iso(task, ramdisk_params)
275 @METRICS.timer('VirtualMediaBoot.clean_up_ramdisk')
276 def clean_up_ramdisk(self, task):
277 """Cleans up the boot of ironic ramdisk.
279 This method cleans up the environment that was setup for booting the
282 :param task: a task from TaskManager.
284 :raises: VirtmediaOperationError if operation failed.
286 self._cleanup_vmedia_boot(task)
288 @METRICS.timer('VirtualMediaBoot.prepare_instance')
289 def prepare_instance(self, task):
290 """Prepares the boot of instance.
292 This method prepares the boot of the instance after reading
293 relevant information from the node's database.
295 :param task: a task from TaskManager.
298 self._cleanup_vmedia_boot(task)
301 iwdi = node.driver_internal_info.get('is_whole_disk_image')
302 if deploy_utils.get_boot_option(node) == "local" or iwdi:
303 manager_utils.node_set_boot_device(task, boot_devices.DISK,
306 driver_internal_info = node.driver_internal_info
307 root_uuid_or_disk_id = driver_internal_info['root_uuid_or_disk_id']
308 self._configure_vmedia_boot(task, root_uuid_or_disk_id)
310 @METRICS.timer('VirtualMediaBoot.clean_up_instance')
311 def clean_up_instance(self, task):
312 """Cleans up the boot of instance.
314 This method cleans up the environment that was setup for booting
317 :param task: a task from TaskManager.
319 :raises: VirtmediaOperationError if operation failed.
321 _remove_share_file(_get_boot_iso_name(task.node))
322 driver_internal_info = task.node.driver_internal_info
323 driver_internal_info.pop('root_uuid_or_disk_id', None)
324 task.node.driver_internal_info = driver_internal_info
326 self._cleanup_vmedia_boot(task)
328 def _configure_vmedia_boot(self, task, root_uuid_or_disk_id):
329 """Configure vmedia boot for the node."""
332 def _set_deploy_boot_device(self, task):
333 """Set the boot device for deployment"""
334 manager_utils.node_set_boot_device(task, boot_devices.CDROM)
336 def _setup_deploy_iso(self, task, ramdisk_options):
337 """Attaches virtual media and sets it as boot device.
339 This method attaches the given deploy ISO as virtual media, prepares the
340 arguments for ramdisk in virtual media floppy.
342 :param task: a TaskManager instance containing the node to act on.
343 :param ramdisk_options: the options to be passed to the ramdisk in virtual
345 :raises: ImageRefValidationFailed if no image service can handle specified
347 :raises: ImageCreationFailed, if it failed while creating the floppy image.
348 :raises: VirtmediaOperationError, if some operation on failed.
349 :raises: InvalidParameterValue if the validation of the
350 PowerInterface or ManagementInterface fails.
352 d_info = task.node.driver_info
354 deploy_iso_href = d_info['virtmedia_deploy_iso']
355 if service_utils.is_image_href_ordinary_file_name(deploy_iso_href):
356 deploy_iso_file = deploy_iso_href
358 deploy_iso_file = _get_deploy_iso_name(task.node)
359 deploy_iso_fullpathname = os.path.join(
360 CONF.remote_image_share_root, deploy_iso_file)
361 images.fetch(task.context, deploy_iso_href, deploy_iso_fullpathname)
363 self._setup_vmedia_for_boot(task, deploy_iso_file, ramdisk_options)
364 self._set_deploy_boot_device(task)
366 def _setup_vmedia_for_boot(self, task, bootable_iso_filename, parameters=None):
367 """Sets up the node to boot from the boot ISO image.
369 This method attaches a boot_iso on the node and passes
370 the required parameters to it via a virtual floppy image.
372 :param task: a TaskManager instance containing the node to act on.
373 :param bootable_iso_filename: a bootable ISO image to attach to.
374 The iso file should be present in NFS/CIFS server.
375 :param parameters: the parameters to pass in a virtual floppy image
376 in a dictionary. This is optional.
377 :raises: ImageCreationFailed, if it failed while creating a floppy image.
378 :raises: VirtmediaOperationError, if attaching a virtual media failed.
380 LOG.info(_translators.log_info("Setting up node %s to boot from virtual media"),
383 self._detach_virtual_cd(task)
384 self._detach_virtual_fd(task)
386 floppy_image_filename = None
388 floppy_image_filename = _prepare_floppy_image(task, parameters)
389 self._attach_virtual_fd(task, floppy_image_filename)
391 if floppy_image_filename:
392 _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename)
394 self._attach_virtual_cd(task, bootable_iso_filename)
396 def _cleanup_vmedia_boot(self, task):
397 """Cleans a node after a virtual media boot.
399 This method cleans up a node after a virtual media boot.
400 It deletes floppy and cdrom images if they exist in NFS/CIFS server.
401 It also ejects both the virtual media cdrom and the virtual media floppy.
403 :param task: a TaskManager instance containing the node to act on.
404 :raises: VirtmediaOperationError if ejecting virtual media failed.
406 LOG.debug("Cleaning up node %s after virtual media boot", task.node.uuid)
409 self._detach_virtual_cd(task)
410 self._detach_virtual_fd(task)
412 _remove_share_file(_get_floppy_image_name(node))
413 _remove_share_file(_get_deploy_iso_name(node))
415 def _attach_virtual_cd(self, task, bootable_iso_filename):
416 """Attaches the given url as virtual media on the node.
418 :param node: an ironic node object.
419 :param bootable_iso_filename: a bootable ISO image to attach to.
420 The iso file should be present in NFS/CIFS server.
421 :raises: VirtmediaOperationError if attaching virtual media failed.
425 def _detach_virtual_cd(self, task):
426 """Detaches virtual cdrom on the node.
428 :param node: an ironic node object.
429 :raises: VirtmediaOperationError if eject virtual cdrom failed.
433 def _attach_virtual_fd(self, task, floppy_image_filename):
434 """Attaches virtual floppy on the node.
436 :param node: an ironic node object.
437 :raises: VirtmediaOperationError if insert virtual floppy failed.
441 def _detach_virtual_fd(self, task):
442 """Detaches virtual media floppy on the node.
444 :param node: an ironic node object.
445 :raises: VirtmediaOperationError if eject virtual media floppy failed.