"""Provide base operations to manage all hard drives, used by VMs"""
# Copyright (c) 2014 - I.T. Dev Ltd
#
# This file is part of MCVirt.
#
# MCVirt is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# MCVirt is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MCVirt. If not, see <http://www.gnu.org/licenses/>
import Pyro4
import os
import xml.etree.ElementTree as ET
from enum import Enum
from mcvirt.exceptions import (HardDriveDoesNotExistException,
StorageTypesCannotBeMixedException,
LogicalVolumeDoesNotExistException,
BackupSnapshotAlreadyExistsException,
BackupSnapshotDoesNotExistException,
ExternalStorageCommandErrorException,
MCVirtCommandException)
from mcvirt.mcvirt_config import MCVirtConfig
from mcvirt.system import System
from mcvirt.auth.permissions import PERMISSIONS
from mcvirt.exceptions import ReachedMaximumStorageDevicesException
from mcvirt.utils import get_hostname
from mcvirt.rpc.pyro_object import PyroObject
from mcvirt.rpc.lock import locking_method
from mcvirt.constants import LockStates
class Driver(Enum):
"""Enums for specifying the hard drive driver type"""
VIRTIO = 'virtio'
IDE = 'ide'
SCSI = 'scsi'
USB = 'usb'
SATA = 'sata'
SD = 'sd'
[docs]class Base(PyroObject):
"""Provides base operations to manage all hard drives, used by VMs"""
# The maximum number of storage devices for the current type
MAXIMUM_DEVICES = 1
# The default driver for the disk
DEFAULT_DRIVER = Driver.IDE.name
# Set default options for snapshotting
SNAPSHOT_SUFFIX = '_snapshot'
SNAPSHOT_SIZE = '500M'
def __init__(self, vm_object, disk_id=None, driver=None):
"""Set member variables"""
self._disk_id = disk_id
self._driver = driver
self.vm_object = vm_object
# If the disk is configured on a VM, obtain
# the details from the VM configuration
for key, value in self.getDiskConfig().iteritems():
setattr(self, key, value)
@property
def config_properties(self):
"""Return the disk object config items"""
return ['disk_id', 'driver']
def __setattr__(self, name, value):
"""Override setattr to ensure that the value of
a disk config item is written to, rather than the
property method
"""
if name in self.config_properties:
name = '_%s' % name
return super(Base, self).__setattr__(name, value)
@property
def disk_id(self):
"""Return the disk ID of the current disk, generating a new one
if there is not already one present
"""
if self._disk_id is None:
self._disk_id = self._get_available_id()
return self._disk_id
@property
def _target_dev(self):
"""Determine the target dev, based on the disk's ID"""
# Use ascii numbers to map 1 => a, 2 => b, etc...
return 'sd' + chr(96 + int(self.disk_id))
@property
def driver(self):
"""Return the disk drive driver name"""
if self._driver is None:
self._driver = self.DEFAULT_DRIVER
return self._driver
[docs] def get_remote_object(self, node_name=None, remote_node=None, registered=True):
"""Obtain an instance of the current hard drive object on a remote node"""
cluster = self._get_registered_object('cluster')
if remote_node is None:
remote_node = cluster.get_remote_node(node_name)
remote_vm_factory = remote_node.get_connection('virtual_machine_factory')
remote_vm = remote_vm_factory.getVirtualMachineByName(self.vm_object.get_name())
remote_hard_drive_factory = remote_node.get_connection('hard_drive_factory')
kwargs = {
'vm_object': remote_vm,
'disk_id': self.disk_id
}
if not registered:
kwargs['storage_type'] = self.get_type()
for config in self.config_properties:
kwargs[config] = getattr(self, config)
hard_drive_object = remote_hard_drive_factory.getObject(**kwargs)
remote_node.annotate_object(hard_drive_object)
return hard_drive_object
def _get_available_id(self):
"""Obtain the next available ID for the VM hard drive, by scanning the IDs
of disks attached to the VM
"""
found_available_id = False
disk_id = 0
vm_config = self.vm_object.get_config_object().get_config()
disks = vm_config['hard_disks']
while (not found_available_id):
disk_id += 1
if not str(disk_id) in disks:
found_available_id = True
# Check that the id is less than 4, as a VM can only have a maximum of 4 disks
if int(disk_id) > self.MAXIMUM_DEVICES:
raise ReachedMaximumStorageDevicesException(
'A maximum of %s hard drives can be mapped to a VM' %
self.MAXIMUM_DEVICES)
return disk_id
def _ensure_exists(self):
"""Ensure the disk exists on the local node"""
if not self._check_exists():
raise HardDriveDoesNotExistException(
'Disk %s for %s does not exist' %
(self.disk_id, self.vm_object.get_name()))
@Pyro4.expose()
[docs] def get_type(self):
"""Return the type of storage for the hard drive"""
return self.__class__.__name__
[docs] def delete(self):
"""Delete the logical volume for the disk"""
self._ensure_exists()
if self.vm_object.isRegisteredLocally():
# Remove from LibVirt, if registered, so that libvirt doesn't
# hold the device open when the storage is removed
self._unregisterLibvirt()
# Remove backing storage
self._removeStorage()
# Remove the hard drive from the MCVirt VM configuration
self.removeFromVirtualMachine(unregister=False)
[docs] def duplicate(self, destination_vm_object):
"""Clone the hard drive and attach it to the new VM object"""
self._ensure_exists()
# Create new disk object, using the same type, size and disk_id
new_disk_object = self.__class__(vm_object=destination_vm_object, disk_id=self.disk_id,
driver=self.driver)
self._register_object(new_disk_object)
new_disk_object.create(self.getSize())
source_drbd_block_device = self._getDiskPath()
destination_drbd_block_device = new_disk_object._getDiskPath()
# Use dd to duplicate the old disk to the new disk
command_args = ('dd', 'if=%s' % source_drbd_block_device,
'of=%s' % destination_drbd_block_device, 'bs=1M')
try:
System.runCommand(command_args)
except MCVirtCommandException, e:
new_disk_object.delete()
raise ExternalStorageCommandErrorException(
"Error whilst duplicating disk logical volume:\n" + str(e)
)
return new_disk_object
@Pyro4.expose()
@locking_method()
def addToVirtualMachine(self, register=True):
"""Add the hard drive to the virtual machine,
and performs the base function on all nodes in the cluster"""
# Update the libvirt domain XML configuration
if self.vm_object.isRegisteredLocally():
self._registerLibvirt()
# Update the VM storage config
self._setVmStorageType()
# Update VM config file
def add_disk_to_config(vm_config):
vm_config['hard_disks'][str(self.disk_id)] = self._getMCVirtConfig()
self.vm_object.get_config_object().update_config(
add_disk_to_config, 'Added disk \'%s\' to \'%s\'' %
(self.disk_id, self.vm_object.get_name())
)
# If the node cluster is initialised, update all remote node configurations
if self._is_cluster_master:
# Create list of nodes that the hard drive was successfully added to
successful_nodes = []
cluster = self._get_registered_object('cluster')
try:
for node in cluster.get_nodes():
remote_disk_object = self.get_remote_object(node, registered=False)
remote_disk_object.addToVirtualMachine()
successful_nodes.append(node)
except Exception:
# If the hard drive fails to be added to a node, remove it from all successful nodes
# and remove from the local node
for node in successful_nodes:
self.get_remote_object(node).removeFromVirtualMachine()
self.removeFromVirtualMachine(unregister=register, all_nodes=False)
raise
@staticmethod
[docs] def isAvailable(pyro_object):
"""Returns whether the storage type is available on the node"""
raise NotImplementedError
@Pyro4.expose()
@locking_method()
def removeFromVirtualMachine(self, unregister=False, all_nodes=True):
"""Remove the hard drive from a VM configuration and perform all nodes
in the cluster"""
# If the VM that the hard drive is attached to is registered on the local
# node, remove the hard drive from the LibVirt configuration
if unregister and self.vm_object.isRegisteredLocally():
self._unregisterLibvirt()
# Update VM config file
def removeDiskFromConfig(vm_config):
del(vm_config['hard_disks'][str(self.disk_id)])
self.vm_object.get_config_object().update_config(
removeDiskFromConfig, 'Removed disk \'%s\' from \'%s\'' %
(self.disk_id, self.vm_object.get_name()))
# If the cluster is initialised, run on all nodes that the VM is available on
if self._is_cluster_master and all_nodes:
cluster = self._get_registered_object('cluster')
for node in cluster.get_nodes():
remote_disk_object = self.get_remote_object(node)
remote_disk_object.removeFromVirtualMachine()
def _unregisterLibvirt(self):
"""Removes the hard drive from the LibVirt configuration for the VM"""
# Update the libvirt domain XML configuration
def updateXML(domain_xml):
device_xml = domain_xml.find('./devices')
disk_xml = device_xml.find(
'./disk/target[@dev="%s"]/..' %
self._target_dev)
device_xml.remove(disk_xml)
# Update libvirt configuration
self.vm_object._editConfig(updateXML)
def _registerLibvirt(self):
"""Register the hard drive with the Libvirt VM configuration"""
def updateXML(domain_xml):
drive_xml = self._generateLibvirtXml()
device_xml = domain_xml.find('./devices')
device_xml.append(drive_xml)
# Update libvirt configuration
self.vm_object._editConfig(updateXML)
def _setVmStorageType(self):
"""Set the VM configuration storage type to the current hard drive type"""
# Ensure VM has not already been configured with disks that
# do not match the type specified
number_of_disks = len(self.vm_object.getHardDriveObjects())
current_storage_type = self.vm_object.get_config_object(
).get_config()['storage_type']
if current_storage_type != self.get_type():
if number_of_disks:
raise StorageTypesCannotBeMixedException(
'The VM (%s) is already configured with %s disks' %
(self.vm_object.get_name(), current_storage_type))
def updateStorageTypeConfig(config):
config['storage_type'] = self.get_type()
self.vm_object.get_config_object().update_config(
updateStorageTypeConfig, 'Updated storage type for \'%s\' to \'%s\'' %
(self.vm_object.get_name(), self.get_type()))
@Pyro4.expose()
@locking_method()
def createLogicalVolume(self, *args, **kwargs):
"""Provides an exposed method for _createLogicalVolume
with permission checking"""
self._get_registered_object('auth').assert_user_type('ClusterUser')
return self._createLogicalVolume(*args, **kwargs)
def _createLogicalVolume(self, name, size, perform_on_nodes=False):
"""Creates a logical volume on the node/cluster"""
volume_group = self._getVolumeGroup()
# Create command list
command_args = ['/sbin/lvcreate', volume_group, '--name', name, '--size', '%sM' % size]
try:
# Create on local node
System.runCommand(command_args)
if perform_on_nodes and self._is_cluster_master:
def remoteCommand(node):
remote_disk = self.get_remote_object(remote_node=node, registered=False)
remote_disk.createLogicalVolume(name=name, size=size)
cluster = self._get_registered_object('cluster')
cluster.run_remote_command(callback_method=remoteCommand,
nodes=self.vm_object._get_remote_nodes())
except MCVirtCommandException, e:
# Remove any logical volumes that had been created if one of them fails
self._removeLogicalVolume(
name,
ignore_non_existent=True,
perform_on_nodes=perform_on_nodes)
raise ExternalStorageCommandErrorException(
"Error whilst creating disk logical volume:\n" + str(e)
)
@Pyro4.expose()
@locking_method()
def removeLogicalVolume(self, *args, **kwargs):
"""Provides an exposed method for _removeLogicalVolume
with permission checking"""
self._get_registered_object('auth').assert_user_type('ClusterUser')
return self._removeLogicalVolume(*args, **kwargs)
def _removeLogicalVolume(self, name, ignore_non_existent=False,
perform_on_nodes=False):
"""Removes a logical volume from the node/cluster"""
# Create command arguments
command_args = ['lvremove', '-f', self._getLogicalVolumePath(name)]
try:
# Determine if logical volume exists before attempting to remove it
if (not (ignore_non_existent and
not self._checkLogicalVolumeExists(name))):
System.runCommand(command_args)
if perform_on_nodes and self._is_cluster_master:
def remoteCommand(node):
remote_disk = self.get_remote_object(remote_node=node, registered=False)
remote_disk.removeLogicalVolume(
name=name, ignore_non_existent=ignore_non_existent
)
cluster = self._get_registered_object('cluster')
cluster.run_remote_command(callback_method=remoteCommand,
nodes=self.vm_object._get_remote_nodes())
except MCVirtCommandException, e:
raise ExternalStorageCommandErrorException(
"Error whilst removing disk logical volume:\n" + str(e)
)
def _get_logical_volume_size(self, name):
"""Obtains the size of a logical volume"""
# Use 'lvs' to obtain the size of the disk
command_args = (
'lvs',
'--nosuffix',
'--noheadings',
'--units',
'm',
'--options',
'lv_size',
self._getLogicalVolumePath(name))
try:
_, command_output, _ = System.runCommand(command_args)
except MCVirtCommandException, e:
raise ExternalStorageCommandErrorException(
"Error whilst obtaining the size of the logical volume:\n" +
str(e))
lv_size = command_output.strip().split('.')[0]
return int(lv_size)
@Pyro4.expose()
@locking_method()
def zeroLogicalVolume(self, *args, **kwargs):
"""Provides an exposed method for _zeroLogicalVolume
with permission checking"""
self._get_registered_object('auth').assert_user_type('ClusterUser')
return self._zeroLogicalVolume(*args, **kwargs)
def _zeroLogicalVolume(self, name, size, perform_on_nodes=False):
"""Blanks a logical volume by filling it with null data"""
# Obtain the path of the logical volume
lv_path = self._getLogicalVolumePath(name)
# Create command arguments
command_args = ['dd', 'if=/dev/zero', 'of=%s' % lv_path, 'bs=1M', 'count=%s' % size,
'conv=fsync', 'oflag=direct']
try:
# Create logical volume on local node
System.runCommand(command_args)
if perform_on_nodes and self._is_cluster_master:
def remoteCommand(node):
remote_disk = self.get_remote_object(remote_node=node, registered=False)
remote_disk.zeroLogicalVolume(name=name, size=size)
cluster = self._get_registered_object('cluster')
cluster.run_remote_command(callback_method=remoteCommand,
nodes=self.vm_object._get_remote_nodes())
except MCVirtCommandException, e:
raise ExternalStorageCommandErrorException(
"Error whilst zeroing logical volume:\n" + str(e)
)
def _ensureLogicalVolumeExists(self, name):
"""Ensures that a logical volume exists, throwing an exception if it does not"""
if not self._checkLogicalVolumeExists(name):
raise LogicalVolumeDoesNotExistException(
'Logical volume %s does not exist on %s' %
(name, get_hostname()))
def _checkLogicalVolumeExists(self, name):
"""Determines if a logical volume exists, returning 1 if present and 0 if not"""
return os.path.lexists(self._getLogicalVolumePath(name))
def _ensureLogicalVolumeActive(self, name):
"""Ensures that a logical volume is active"""
if not self._checkLogicalVolumeActive(name):
raise LogicalVolumeIsNotActive(
'Logical volume %s is not active on %s' %
(name, get_hostname()))
def _checkLogicalVolumeActive(self, name):
"""Checks that a logical volume is active"""
return os.path.exists(self._getLogicalVolumePath(name))
@Pyro4.expose()
@locking_method()
def activateLogicalVolume(self, *args, **kwargs):
"""Provides an exposed method for _activateLogicalVolume
with permission checking"""
self._get_registered_object('auth').assert_user_type('ClusterUser')
return self._activateLogicalVolume(*args, **kwargs)
def _activateLogicalVolume(self, name, perform_on_nodes=False):
"""Activates a logical volume on the node/cluster"""
# Obtain logical volume path
lv_path = self._getLogicalVolumePath(name)
# Create command arguments
command_args = ['lvchange', '-a', 'y', '--yes', lv_path]
try:
# Run on the local node
System.runCommand(command_args)
if perform_on_nodes and self._is_cluster_master:
def remoteCommand(node):
remote_disk = self.get_remote_object(remote_node=node, registered=False)
remote_disk.activateLogicalVolume(name=name)
cluster = self._get_registered_object('cluster')
cluster.run_remote_command(callback_method=remoteCommand,
nodes=self.vm_object._get_remote_nodes())
except MCVirtCommandException, e:
raise ExternalStorageCommandErrorException(
"Error whilst activating logical volume:\n" + str(e)
)
[docs] def createBackupSnapshot(self):
"""Creates a snapshot of the logical volume for backing up and locks the VM"""
self._ensure_exists()
# Ensure the user has permission to delete snapshot backups
self._get_registered_object('auth').assert_permission(
PERMISSIONS.BACKUP_VM,
self.vm_object
)
# Ensure VM is registered locally
self.vm_object.ensureRegisteredLocally()
# Obtain logical volume names/paths
backup_volume_path = self._getLogicalVolumePath(
self._getBackupLogicalVolume())
snapshot_logical_volume = self._getBackupSnapshotLogicalVolume()
# Determine if logical volume already exists
if self._checkLogicalVolumeActive(snapshot_logical_volume):
raise BackupSnapshotAlreadyExistsException(
'The backup snapshot for \'%s\' already exists: %s' %
(backup_volume_path, snapshot_logical_volume)
)
# Lock the VM
self.vm_object._setLockState(LockStates.LOCKED)
try:
System.runCommand(['lvcreate', '--snapshot', backup_volume_path,
'--name', self._getBackupSnapshotLogicalVolume(),
'--size', self.SNAPSHOT_SIZE])
return self._getLogicalVolumePath(snapshot_logical_volume)
except:
self.vm_object._setLockState(LockStates.UNLOCKED)
raise
[docs] def deleteBackupSnapshot(self):
"""Deletes the backup snapshot for the disk and unlocks the VM"""
self._ensure_exists()
# Ensure the user has permission to delete snapshot backups
self._get_registered_object('auth').assert_permission(
PERMISSIONS.BACKUP_VM,
self.vm_object
)
# Ensure the snapshot logical volume exists
if not self._checkLogicalVolumeActive(self._getBackupSnapshotLogicalVolume()):
raise BackupSnapshotDoesNotExistException(
'The backup snapshot for \'%s\' does not exist' %
self._getLogicalVolumePath(config._getBackupLogicalVolume())
)
System.runCommand(['lvremove', '-f',
self._getLogicalVolumePath(self._getBackupSnapshotLogicalVolume())])
# Unlock the VM
self.vm_object._setLockState(LockStates.UNLOCKED)
@Pyro4.expose()
@locking_method()
def increaseSize(self, increase_size):
"""Increases the size of a VM hard drive, given the size to increase the drive by"""
raise NotImplementedError
def _check_exists(self):
"""Checks if the disk exists"""
raise NotImplementedError
[docs] def clone(self, destination_vm_object):
"""Clone a VM, using snapshotting, attaching it to the new VM object"""
raise NotImplementedError
[docs] def create(self):
"""Creates a new disk image, attaches the disk to the VM and records the disk
in the VM configuration"""
raise NotImplementedError
[docs] def activateDisk(self):
"""Activates the storage volume"""
raise NotImplementedError
[docs] def deactivateDisk(self):
"""Deactivates the storage volume"""
raise NotImplementedError
[docs] def preMigrationChecks(self, destination_node):
"""Determines if the disk is in a state to allow the attached VM
to be migrated to another node"""
raise NotImplementedError
[docs] def preOnlineMigration(self):
"""Performs required tasks in order
for the underlying VM to perform an
online migration"""
raise NotImplementedError
[docs] def postOnlineMigration(self):
"""Performs post tasks after a VM
has performed an online migration"""
raise NotImplementedError
[docs] def getSize(self):
"""Gets the size of the disk (in MB)"""
raise NotImplementedError
[docs] def move(self, destination_node, source_node):
"""Moves the storage to another node in the cluster"""
raise NotImplementedError
def _getVolumeGroup(self):
"""Returns the node MCVirt volume group"""
return MCVirtConfig().get_config()['vm_storage_vg']
[docs] def getDiskConfig(self):
"""Returns the disk configuration for the hard drive"""
vm_config = self.vm_object.get_config_object().get_config()
if str(self.disk_id) in vm_config['hard_disks']:
return vm_config['hard_disks'][str(self.disk_id)]
else:
return {}
def _getLogicalVolumePath(self, name):
"""Returns the full path of a given logical volume"""
volume_group = self._getVolumeGroup()
return '/dev/' + volume_group + '/' + name
def _generateLibvirtXml(self):
"""Creates a basic libvirt XML configuration for the connection to the disk"""
# Create the base disk XML element
device_xml = ET.Element('disk')
device_xml.set('type', 'block')
device_xml.set('device', 'disk')
# Configure the interface driver to the disk
driver_xml = ET.SubElement(device_xml, 'driver')
driver_xml.set('name', 'qemu')
driver_xml.set('type', 'raw')
driver_xml.set('cache', self.CACHE_MODE)
# Configure the source of the disk
source_xml = ET.SubElement(device_xml, 'source')
source_xml.set('dev', self._getDiskPath())
# Configure the target
target_xml = ET.SubElement(device_xml, 'target')
target_xml.set('dev', '%s' % self._target_dev)
target_xml.set('bus', self._getLibvirtDriver())
return device_xml
def _getLibvirtDriver(self):
"""Returns the libvirt name of the driver for the disk"""
return Driver[self.driver].value
@Pyro4.expose()
[docs] def getDiskPath(self):
"""Exposed method for _getDiskPath"""
self._get_registered_object('auth').assert_permission(
PERMISSIONS.MANAGE_CLUSTER
)
return self._getDiskPath()
def _getDiskPath(self):
"""Returns the path of the raw disk image"""
raise NotImplementedError
def _getMCVirtConfig(self):
"""Returns the MCVirt configuration for the hard drive object"""
config = {
'driver': self.driver
}
return config
def _getBackupLogicalVolume(self):
"""Returns the storage device for the backup"""
raise NotImplementedError
def _getBackupSnapshotLogicalVolume(self):
"""Returns the logical volume name for the backup snapshot"""
raise NotImplementedError