#!/usr/bin/env python
#
# add_disk.py - Implements "cot add-disk" command
#
# August 2013, Glenn F. Matthews
# Copyright (c) 2013-2015 the COT project developers.
# See the COPYRIGHT.txt file at the top-level directory of this distribution
# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt.
#
# This file is part of the Common OVF Tool (COT) project.
# It is subject to the license terms in the LICENSE.txt file found in the
# top-level directory of this distribution and at
# https://github.com/glennmatthews/cot/blob/master/LICENSE.txt. No part
# of COT, including this file, may be copied, modified, propagated, or
# distributed except according to the terms contained in the LICENSE.txt file.
"""Module for adding disks to VMs.
.. autosummary::
COTAddDisk
add_disk_worker
"""
import logging
import os.path
from .data_validation import InvalidInputError, ValueUnsupportedError
from .data_validation import check_for_conflict, device_address, match_or_die
from .submodule import COTSubmodule
logger = logging.getLogger(__name__)
[docs]class COTAddDisk(COTSubmodule):
"""Add or replace a disk in a virtual machine.
Inherited attributes:
:attr:`~COTGenericSubmodule.UI`,
:attr:`~COTSubmodule.package`,
:attr:`~COTSubmodule.output`
Attributes:
:attr:`disk_image`,
:attr:`type`,
:attr:`file_id`,
:attr:`controller`,
:attr:`subtype`,
:attr:`address`,
:attr:`diskname`,
:attr:`description`
"""
def __init__(self, UI):
"""Instantiate this submodule with the given UI."""
super(COTAddDisk, self).__init__(UI)
self._disk_image = None
self.type = None
"""Disk type ('harddisk' or 'cdrom')."""
self.subtype = None
"""Controller subtype, such as "virtio"."""
self.file_id = None
"""File identifier to map disk to file."""
self._controller = None
self._address = None
self.diskname = None
"""Name string for the disk."""
self.description = None
"""Description of the disk."""
@property
def disk_image(self):
"""Path to disk image file to add to the VM.
:raises: :exc:`.InvalidInputError` if the file does not exist.
"""
return self._disk_image
@disk_image.setter
def disk_image(self, value):
if not os.path.exists(value):
raise InvalidInputError("Specified disk '{0}' does not exist!"
.format(value))
self._disk_image = value
@property
def address(self):
"""Disk device address on controller (``1:0``, etc.).
:raises: :exc:`.InvalidInputError`,
see :meth:`validate_controller_address`
"""
return self._address
@address.setter
def address(self, value):
logger.info("Setting address to '{0}'".format(value))
self.validate_controller_address(self.controller, value)
self._address = value
@property
def controller(self):
"""Disk controller type (``ide``, ``scsi``).
:raises: :exc:`.InvalidInputError`,
see :meth:`validate_controller_address`
"""
return self._controller
@controller.setter
def controller(self, value):
logger.info("Setting controller to '{0}'".format(value))
self.validate_controller_address(value, self.address)
self._controller = value
[docs] def validate_controller_address(self, controller, address):
"""Check validity of the given address string for the given controller.
Helper method for the :attr:`controller`/:attr:`address` setters.
:param str controller: ``'ide'`` or ``'scsi'``
:param str address: A string like '0:0' or '2:10'
:raises: :exc:`.InvalidInputError` if the address/controller combo
is invalid.
"""
logger.info("validate_controller_address: {0}, {1}"
.format(controller, address))
if controller is not None and address is not None:
logger.info("Validating controller/address combo")
ctrl_addr = address.split(":")[0]
disk_addr = address.split(":")[1]
if controller == "scsi" and (int(ctrl_addr) > 3 or
int(disk_addr) > 15):
raise InvalidInputError(
"SCSI disk address must be between 0:0 and 3:15")
elif controller == "ide" and (int(ctrl_addr) > 1 or
int(disk_addr) > 1):
raise InvalidInputError(
"IDE disk address must be between 0:0 and 1:1")
[docs] def ready_to_run(self):
"""Check whether the module is ready to :meth:`run`.
:returns: ``(True, ready_message)`` or ``(False, reason_why_not)``
"""
if self.disk_image is None:
return False, "DISK_IMAGE is a mandatory argument!"
elif self.address is not None and self.controller is None:
return False, ("When specifying an address you must also "
"specify the controller type")
return super(COTAddDisk, self).ready_to_run()
[docs] def run(self):
"""Do the actual work of this submodule.
:raises InvalidInputError: if :func:`ready_to_run` reports ``False``
"""
super(COTAddDisk, self).run()
add_disk_worker(self.vm,
UI=self.UI,
DISK_IMAGE=self.disk_image,
type=self.type,
subtype=self.subtype,
file_id=self.file_id,
controller=self.controller,
address=self.address,
diskname=self.diskname,
description=self.description)
[docs] def create_subparser(self, parent, storage):
"""Add subparser for the CLI of this submodule.
:param object parent: Subparser grouping object returned by
:meth:`ArgumentParser.add_subparsers`
:param dict storage: Dict of { 'label': subparser } to be updated with
subparser(s) created, if any.
"""
p = parent.add_parser(
'add-disk', add_help=False,
usage=self.UI.fill_usage("add-disk", [
"DISK_IMAGE PACKAGE [-o OUTPUT] [-f FILE_ID] \
[-t {harddisk,cdrom}] [-c {ide,scsi}] [-s SUBTYPE] [-a ADDRESS] \
[-d DESCRIPTION] [-n DISKNAME]"
]),
help="""Add a disk image to an OVF package and map it as a disk
in the guest environment""",
description="""
Add or replace a disk image in the specified OVF or OVA.
If the specified disk image, controller/address, file-id, and/or instance
match an existing entry in the OVF, will replace the existing disk with
the provided file (prompting for confirmation if --force was not set);
otherwise, will create a new disk entry.""")
group = p.add_argument_group("general options")
group.add_argument('-h', '--help', action='help',
help="""Show this help message and exit""")
group.add_argument('-o', '--output',
help="""Name/path of new OVF/OVA package to """
"""create instead of updating the existing OVF""")
group = p.add_argument_group("disk-related options")
group.add_argument('-f', '--file-id',
help="""Disk image file ID string within the OVF """
"""package (default: use disk image filename)""")
group.add_argument('-t', '--type',
choices=['harddisk', 'cdrom'],
help="""Disk type (default: files ending in """
""".vmdk/.raw/.qcow2/.img will use harddisk """
"""and files ending in .iso will use cdrom)""")
group = p.add_argument_group("controller-related options")
group.add_argument('-c', '--controller',
choices=['ide', 'scsi'],
help="""Disk controller type (default: """
"""determined by disk type and platform)""")
group.add_argument('-a', '--address', type=device_address,
help="""Address of the disk, such as "1:0". """
"""Requires that --controller be explicitly set. """
"""(default: use first unused address on the """
"""controller)""")
group.add_argument('-s', '--subtype',
help="""Disk controller subtype such as "virtio" """
"""or "lsilogic".""")
group = p.add_argument_group("descriptive options")
group.add_argument('-d', '--description',
help="""Description of this disk (optional)""")
group.add_argument('-n', '--name', dest='diskname',
help="""Name of this disk (default: """
""""Hard disk #" or "CD-ROM #" as appropriate)""")
p.add_argument('DISK_IMAGE',
help="""Disk image file to add to the package""")
p.add_argument('PACKAGE',
help="""OVF descriptor or OVA file to edit""")
p.set_defaults(instance=self)
storage['add-disk'] = p
[docs]def add_disk_worker(vm,
UI,
DISK_IMAGE,
type=None,
file_id=None,
controller=None,
subtype=None,
address=None,
diskname=None,
description=None):
"""Worker function for actually adding the disk.
All parameters except ``vm``, ``UI``, and ``DISK_IMAGE`` are optional
and will be automatically determined by COT if unspecified.
:param vm: The virtual machine being edited.
:type vm: :class:`~COT.ovf.OVF` or other
:class:`~COT.vm_description.VMDescription` subclass
:param UI: User interface in effect.
:type UI: instance of :class:`~COT.ui_shared.UI` or subclass.
:param str DISK_IMAGE: path to disk image to add to the VM.
:param str type: Disk type: ``'cdrom'`` or ``'harddisk'``.
If not specified, will be derived automatically from the
DISK_IMAGE file name extension.
:param str file_id: Identifier of the disk file in the VM. If not
specified, the VM will automatically derive an appropriate value.
:param str controller: Disk controller type: ``'ide'`` or ``'scsi'``.
If not specified, will be derived from the `type` and the
`platform` of the given `vm`.
:param str subtype: Controller subtype ('virtio', 'lsilogic', etc.)
:param str address: Disk device address on its controller
(such as ``'1:0'``). If this matches an existing disk device,
that device will be overwritten. If not specified, the first
available address not already occupied by an existing device
will be selected.
:param str diskname: Name for disk device
:param str description: Description of disk device
"""
if type is None:
disk_extension = os.path.splitext(DISK_IMAGE)[1]
ext_type_map = {
'.iso': 'cdrom',
'.vmdk': 'harddisk',
'.raw': 'harddisk',
'.qcow2': 'harddisk',
'.img': 'harddisk',
}
try:
type = ext_type_map[disk_extension]
except KeyError:
raise InvalidInputError(
"Unable to guess disk type for file '{0}' "
"from its extension '{1}'.\n"
"Known extensions are {2}\n"
"Please specify '--type harddisk' or '--type cdrom'."
.format(DISK_IMAGE, disk_extension,
ext_type_map.keys()))
logger.warning("New disk type not specified, guessing it should "
"be '{0}' based on file extension".format(type))
# Convert the disk to a new format if needed...
DISK_IMAGE = vm.convert_disk_if_needed(DISK_IMAGE, type)
disk_file = os.path.basename(DISK_IMAGE)
# A disk is defined by up to four different sections in the OVF:
#
# File (references the actual disk image file)
# Disk (references the File, only used for HD not CD-ROM)
# Item (defines the SCSI/IDE controller)
# Item (defines the disk drive, links to controller and File or Disk)
#
# For each of these four sections, we need to know whether to add
# a new one or overwrite an existing one. Depending on the user
# arguments, we can do this by as many as three different approaches:
#
# 1) Check whether the DISK_IMAGE file name matches an existing File
# in the OVF (and from there, find the associated Disk and Items)
# 2) Check whether the --file-id matches an existing File and/or Disk
# in the OVF (and from there, find the associated Items)
# 3) Check whether --controller and/or --address match existing Items
# in the OVF (and from there, find the associated Disk and/or File)
#
# Where it gets extra fun is if the user has specified more than one
# of the above arguments - in which case we need to make sure that
# all relevant approaches agree on what sections we're talking about...
# 1) Check whether the DISK_IMAGE file name matches an existing File
# in the OVF (and from there, find the associated Disk and Items)
(f1, d1, ci1, di1) = vm.search_from_filename(disk_file)
# 2) Check whether the --file-id matches an existing File and/or Disk
# in the OVF (and from there, find the associated Items)
# In the case where no file_id is specified, we may default to the
# filename, so check that instead
if file_id is not None:
(f2, d2, ci2, di2) = vm.search_from_file_id(file_id)
else:
(f2, d2, ci2, di2) = vm.search_from_file_id(disk_file)
# 3) Check whether the --controller and --address match existing Items
# in the OVF (and from there, find the associated Disk and/or File)
(f3, d3, ci3, di3) = vm.search_from_controller(controller,
address)
file = check_for_conflict("File to overwrite", [f1, f2, f3])
disk = check_for_conflict("Disk to overwrite", [d1, d2, d3])
ctrl_item = check_for_conflict("controller Item to use",
[ci1, ci2, ci3])
disk_item = check_for_conflict("disk Item to overwrite",
[di1, di2, di3])
# Ok, we now have confirmed that we have at most one of each of these
# four objects. Now it's time for some sanity checking...
if file is not None:
if file_id is not None:
match_or_die("File id", vm.get_id_from_file(file),
"--file-id", file_id)
# Should never fail this test if the above logic was sound...
if disk is not None:
match_or_die("File id", vm.get_id_from_file(file),
"Disk fileRef", vm.get_file_ref_from_disk(disk))
if disk is not None:
if file_id is not None:
match_or_die("Disk fileRef", vm.get_file_ref_from_disk(disk),
"--file-id", file_id)
if file is None:
# This will happen if we're replacing a placeholder entry
# (disk exists but has no associated file)
logger.verbose("Found Disk but not File - maybe placeholder?")
if disk_item is not None:
UI.confirm_or_die("Existing disk Item is a {0}. Change it to a {1}?"
.format(vm.get_type_from_device(disk_item),
type))
vm.check_sanity_of_disk_device(disk, file, disk_item, ctrl_item)
if ctrl_item is not None:
if controller is not None:
match_or_die("controller type",
vm.get_type_from_device(ctrl_item),
"--controller", controller)
else:
controller = vm.get_type_from_device(ctrl_item)
if controller != 'ide' and controller != 'scsi':
raise ValueUnsupportedError("controller ResourceType",
controller,
"'ide' or 'scsi'")
logger.info("Guessing controller type '{0}' from existing Item"
.format(controller))
else:
# If the user didn't tell us which controller type they wanted,
# and we didn't find a controller item based on existing file/disk,
# then we need to guess which type of controller we need,
# based on the platform and the disk type.
if controller is None:
controller = vm.platform.controller_type_for_device(type)
logger.warning("Guessing controller type should be {0} "
"based on disk type {1} and platform {2}"
.format(controller, type,
vm.platform.__name__))
if address is None:
# We didn't find a specific controller from the user info,
# but also the user didn't request a specific controller.
# So try and just look for any controller of the right type
(ctrl_item, address) = vm.find_open_controller(
controller)
logger.debug("Validation of existing data complete")
# Whew! Everything looks sane!
if file is not None:
UI.confirm_or_die("Replace existing file {0} with {1}?"
.format(vm.get_path_from_file(file),
DISK_IMAGE))
logger.warning("Overwriting existing File in OVF")
if file is None and (disk is not None or disk_item is not None):
UI.confirm_or_die(
"Add disk file to existing (but empty) {0} drive?"
.format(type))
if disk is not None:
logger.warning("Overwriting existing Disk in OVF")
if disk_item is not None:
# We'll overwrite the existing disk Item instead of deleting
# and recreating it, in order to preserve things like Description
logger.warning("Overwriting existing disk Item in OVF")
if ctrl_item is not None:
if subtype is not None:
curr_subtype = vm.get_subtype_from_device(ctrl_item)
if curr_subtype is not None and curr_subtype != subtype:
UI.confirm_or_die("Change {0} controller subtype from "
"'{1}' to '{2}'?".format(controller,
curr_subtype,
subtype))
else:
# In most cases we are NOT adding a new controller, so be safe...
UI.confirm_or_die("Add new {0} controller to OVF descriptor?"
.format(controller.upper()))
if subtype is None:
# Look for any existing controller of this type;
# if found, re-use its subtype for consistency
logger.verbose("Looking for subtype of existing controllers")
subtype = vm.get_common_subtype(controller)
# OK - let's add things!
if file_id is None and file is not None:
file_id = vm.get_id_from_file(file)
if file_id is None and disk is not None:
file_id = vm.get_file_ref_from_disk(disk)
if file_id is None:
file_id = disk_file
# First, the File
file = vm.add_file(DISK_IMAGE, file_id, file, disk)
# Next, the Disk
disk = vm.add_disk(DISK_IMAGE, file_id, type, disk)
# Next, the controller (if needed)
if address is not None:
ctrl_addr = address.split(":")[0]
disk_addr = address.split(":")[1]
else:
# let VM choose controller address if necessary
ctrl_addr = None
disk_addr = None
ctrl_item = vm.add_controller_device(controller, subtype,
ctrl_addr, ctrl_item)
# Finally, the disk Item
vm.add_disk_device(type, disk_addr, diskname,
description, disk, file, ctrl_item, disk_item)