Seed code for ironic_virtmedia_driver
[ta/ironic-virtmedia-driver.git] / src / ironic_virtmedia_driver / virtmedia.py
1 # Copyright 2019 Nokia
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 #
15
16 import os
17 import shutil
18 import tempfile
19 import tarfile
20
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
25
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
38
39 LOG = logging.getLogger(__name__)
40
41 METRICS = metrics_utils.get_metrics_logger(__name__)
42
43 REQUIRED_PROPERTIES = {
44     'virtmedia_deploy_iso': _("Deployment ISO image file name. "
45                          "Required."),
46 }
47
48 COMMON_PROPERTIES = REQUIRED_PROPERTIES
49
50
51 def _parse_config_option():
52     """Parse config file options.
53
54     This method checks config file options validity.
55
56     :raises: InvalidParameterValue, if config option has invalid value.
57     """
58     error_msgs = []
59     if not os.path.isdir(CONF.remote_image_share_root):
60         error_msgs.append(
61             _("Value '%s' for remote_image_share_root isn't a directory "
62               "or doesn't exist.") %
63             CONF.remote_image_share_root)
64     if error_msgs:
65         msg = (_("The following errors were encountered while parsing "
66                  "config file:%s") % error_msgs)
67         raise exception.InvalidParameterValue(msg)
68
69
70 def _parse_driver_info(node):
71     """Gets the driver specific Node deployment info.
72
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.
76
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
80         missing.
81     :raises: InvalidParameterValue, if any of the parameters have invalid
82         value.
83     """
84     d_info = node.driver_info
85     deploy_info = {}
86
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)
91
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)
101
102     return deploy_info
103
104 def _parse_deploy_info(node):
105     """Gets the instance and driver specific Node deployment info.
106
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.
110
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
114         missing.
115     :raises: InvalidParameterValue, if any of the parameters have invalid
116         value.
117     """
118     deploy_info = {}
119     deploy_info.update(deploy_utils.get_image_instance_info(node))
120     deploy_info.update(_parse_driver_info(node))
121
122     return deploy_info
123
124 def _get_deploy_iso_name(node):
125     """Returns the deploy ISO file name for a given node.
126
127     :param node: the node for which ISO file name is to be provided.
128     """
129     return "deploy-%s.iso" % node.name
130
131 def _get_boot_iso_name(node):
132     """Returns the boot ISO file name for a given node.
133
134     :param node: the node for which ISO file name is to be provided.
135     """
136     return "boot-%s.iso" % node.uuid
137
138 def _get_floppy_image_name(node):
139     """Returns the floppy image name for a given node.
140
141     :param node: the node for which image name is to be provided.
142     """
143     return "image-%s.img" % node.name
144
145
146 def _prepare_floppy_image(task, params):
147     """Prepares the floppy image for passing the parameters.
148
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.
152
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.
159     """
160     floppy_filename = _get_floppy_image_name(task.node)
161     floppy_fullpathname = os.path.join(
162         CONF.remote_image_share_root, floppy_filename)
163
164     with tempfile.NamedTemporaryFile() as vfat_image_tmpfile_obj:
165         images.create_vfat_image(vfat_image_tmpfile_obj.name,
166                                  parameters=params)
167         try:
168             shutil.copyfile(vfat_image_tmpfile_obj.name,
169                             floppy_fullpathname)
170         except IOError as e:
171             operation = _("Copying floppy image file")
172             raise virtmedia_exception.VirtmediaOperationError(
173                 operation=operation, error=e)
174
175     return floppy_filename
176
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
180         64K block tar file.
181     """
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' 
185
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))
189     tar.close()
190
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')
193
194     os.remove(tar_file_path)
195
196 def _remove_share_file(share_filename):
197     """Remove given file from the share file system.
198
199     :param share_filename: a file name to be removed.
200     """
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)
205
206 class VirtmediaBoot(base.BootInterface):
207     """Implementation of a boot interface using Virtual Media."""
208
209     def __init__(self):
210         """Constructor of VirtualMediaBoot.
211
212         :raises: InvalidParameterValue, if config option has invalid value.
213         """
214         super(VirtmediaBoot, self).__init__()
215
216     def get_properties(self):
217         return COMMON_PROPERTIES
218
219     @METRICS.timer('VirtualMediaBoot.validate')
220     def validate(self, task):
221         """Validate the deployment information for the task's node.
222
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.
229         """
230         d_info = _parse_deploy_info(task.node)
231         if task.node.driver_internal_info.get('is_whole_disk_image'):
232             props = []
233         elif service_utils.is_glance_image(d_info['image_source']):
234             props = ['kernel_id', 'ramdisk_id']
235         else:
236             props = ['kernel', 'ramdisk']
237         deploy_utils.validate_image_properties(task.context, d_info,
238                                                props)
239
240     @METRICS.timer('VirtualMediaBoot.prepare_ramdisk')
241     def prepare_ramdisk(self, task, ramdisk_params):
242         """Prepares the deploy ramdisk using virtual media.
243
244         Prepares the options for the deployment ramdisk, sets the node to boot
245         from virtual media cdrom.
246
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
250                  specified href.
251         :raises: ImageCreationFailed, if it failed while creating the floppy
252                  image.
253         :raises: InvalidParameterValue if the validation of the
254                  PowerInterface or ManagementInterface fails.
255         :raises: VirtmediaOperationError, if some operation fails.
256         """
257
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.
262
263         if (task.node.provision_state != states.DEPLOYING and
264                 task.node.provision_state != states.CLEANING):
265             return
266
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')
270         if os_net_config:
271             ramdisk_params['os_net_config'] = os_net_config
272
273         self._setup_deploy_iso(task, ramdisk_params)
274
275     @METRICS.timer('VirtualMediaBoot.clean_up_ramdisk')
276     def clean_up_ramdisk(self, task):
277         """Cleans up the boot of ironic ramdisk.
278
279         This method cleans up the environment that was setup for booting the
280         deploy ramdisk.
281
282         :param task: a task from TaskManager.
283         :returns: None
284         :raises: VirtmediaOperationError if operation failed.
285         """
286         self._cleanup_vmedia_boot(task)
287
288     @METRICS.timer('VirtualMediaBoot.prepare_instance')
289     def prepare_instance(self, task):
290         """Prepares the boot of instance.
291
292         This method prepares the boot of the instance after reading
293         relevant information from the node's database.
294
295         :param task: a task from TaskManager.
296         :returns: None
297         """
298         self._cleanup_vmedia_boot(task)
299
300         node = task.node
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,
304                                                persistent=True)
305         else:
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)
309
310     @METRICS.timer('VirtualMediaBoot.clean_up_instance')
311     def clean_up_instance(self, task):
312         """Cleans up the boot of instance.
313
314         This method cleans up the environment that was setup for booting
315         the instance.
316
317         :param task: a task from TaskManager.
318         :returns: None
319         :raises: VirtmediaOperationError if operation failed.
320         """
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
325         task.node.save()
326         self._cleanup_vmedia_boot(task)
327
328     def _configure_vmedia_boot(self, task, root_uuid_or_disk_id):
329         """Configure vmedia boot for the node."""
330         return
331
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)
335
336     def _setup_deploy_iso(self, task, ramdisk_options):
337         """Attaches virtual media and sets it as boot device.
338
339         This method attaches the given deploy ISO as virtual media, prepares the
340         arguments for ramdisk in virtual media floppy.
341
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
344             media floppy.
345         :raises: ImageRefValidationFailed if no image service can handle specified
346            href.
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.
351         """
352         d_info = task.node.driver_info
353
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
357         else:
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)
362
363         self._setup_vmedia_for_boot(task, deploy_iso_file, ramdisk_options)
364         self._set_deploy_boot_device(task)
365
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.
368
369         This method attaches a boot_iso on the node and passes
370         the required parameters to it via a virtual floppy image.
371
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.
379         """
380         LOG.info(_translators.log_info("Setting up node %s to boot from virtual media"),
381                  task.node.uuid)
382
383         self._detach_virtual_cd(task)
384         self._detach_virtual_fd(task)
385
386         floppy_image_filename = None
387         if parameters:
388             floppy_image_filename = _prepare_floppy_image(task, parameters)
389             self._attach_virtual_fd(task, floppy_image_filename)
390
391         if floppy_image_filename:
392             _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename)
393
394         self._attach_virtual_cd(task, bootable_iso_filename)
395
396     def _cleanup_vmedia_boot(self, task):
397         """Cleans a node after a virtual media boot.
398
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.
402
403         :param task: a TaskManager instance containing the node to act on.
404         :raises: VirtmediaOperationError if ejecting virtual media failed.
405         """
406         LOG.debug("Cleaning up node %s after virtual media boot", task.node.uuid)
407
408         node = task.node
409         self._detach_virtual_cd(task)
410         self._detach_virtual_fd(task)
411
412         _remove_share_file(_get_floppy_image_name(node))
413         _remove_share_file(_get_deploy_iso_name(node))
414
415     def _attach_virtual_cd(self, task, bootable_iso_filename):
416         """Attaches the given url as virtual media on the node.
417
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.
422         """
423         return
424
425     def _detach_virtual_cd(self, task):
426         """Detaches virtual cdrom on the node.
427
428         :param node: an ironic node object.
429         :raises: VirtmediaOperationError if eject virtual cdrom failed.
430         """
431         return
432
433     def _attach_virtual_fd(self, task, floppy_image_filename):
434         """Attaches virtual floppy on the node.
435
436         :param node: an ironic node object.
437         :raises: VirtmediaOperationError if insert virtual floppy failed.
438         """
439         return
440
441     def _detach_virtual_fd(self, task):
442         """Detaches virtual media floppy on the node.
443
444         :param node: an ironic node object.
445         :raises: VirtmediaOperationError if eject virtual media floppy failed.
446         """
447         return