|
@@ -31,264 +31,471 @@ import os.path
|
|
|
import re
|
|
|
import subprocess
|
|
|
|
|
|
-import lxml.etree
|
|
|
+from qubes.storage import Pool, StoragePoolException, Volume
|
|
|
|
|
|
-import qubes
|
|
|
-import qubes.config
|
|
|
-import qubes.storage
|
|
|
-import qubes.vm.templatevm
|
|
|
+BLKSIZE = 512
|
|
|
|
|
|
|
|
|
-class XenStorage(qubes.storage.Storage):
|
|
|
- '''Class for VM storage of Xen VMs.
|
|
|
- '''
|
|
|
+class XenPool(Pool):
|
|
|
+ ''' File based 'original' disk implementation '''
|
|
|
+ driver = 'xen'
|
|
|
+
|
|
|
+ def __init__(self, name=None, dir_path=None):
|
|
|
+ super(XenPool, self).__init__(name=name)
|
|
|
+ assert dir_path, "No pool dir_path specified"
|
|
|
+ self.dir_path = os.path.normpath(dir_path)
|
|
|
+
|
|
|
+ def clone(self, source, target):
|
|
|
+ ''' Clones the volume if the `source.pool` if the source is a
|
|
|
+ :py:class:`XenVolume`.
|
|
|
+ '''
|
|
|
+ if issubclass(XenVolume, source.__class__):
|
|
|
+ raise StoragePoolException('Volumes %s and %s use different pools'
|
|
|
+ % (source.__class__, target.__class__))
|
|
|
+
|
|
|
+ if source.volume_type not in ['origin', 'read-write']:
|
|
|
+ return target
|
|
|
+
|
|
|
+ copy_file(source.vid, target.vid)
|
|
|
+ return target
|
|
|
+
|
|
|
+ def create(self, volume, source_volume=None):
|
|
|
+ _type = volume.volume_type
|
|
|
+ size = volume.size
|
|
|
+ if _type == 'origin':
|
|
|
+ create_sparse_file(volume.path_origin, size)
|
|
|
+ create_sparse_file(volume.path_cow, size)
|
|
|
+ elif _type in ['read-write'] and source_volume:
|
|
|
+ copy_file(source_volume.path, volume.path)
|
|
|
+ elif _type in ['read-write', 'volatile']:
|
|
|
+ create_sparse_file(volume.path, size)
|
|
|
+
|
|
|
+ return volume
|
|
|
+
|
|
|
+ @property
|
|
|
+ def config(self):
|
|
|
+ return {
|
|
|
+ 'name': self.name,
|
|
|
+ 'dir_path': self.dir_path,
|
|
|
+ 'driver': XenPool.driver,
|
|
|
+ }
|
|
|
+
|
|
|
+ def resize(self, volume, size):
|
|
|
+ ''' Expands volume, throws
|
|
|
+ :py:class:`qubst.storage.StoragePoolException` if given size is
|
|
|
+ less than current_size
|
|
|
+ '''
|
|
|
+ _type = volume.volume_type
|
|
|
+ if _type not in ['origin', 'read-write', 'volatile']:
|
|
|
+ raise StoragePoolException('Can not resize a %s volume %s' %
|
|
|
+ (_type, volume.vid))
|
|
|
+
|
|
|
+ if size <= volume.size:
|
|
|
+ raise StoragePoolException(
|
|
|
+ 'For your own safety, shrinking of %s is'
|
|
|
+ ' disabled. If you really know what you'
|
|
|
+ ' are doing, use `truncate` on %s manually.' %
|
|
|
+ (volume.name, volume.vid))
|
|
|
+
|
|
|
+ if _type == 'origin':
|
|
|
+ path = volume.path_origin
|
|
|
+ elif _type in ['read-write', 'volatile']:
|
|
|
+ path = volume.path
|
|
|
+
|
|
|
+ with open(path, 'a+b') as fd:
|
|
|
+ fd.truncate(size)
|
|
|
+
|
|
|
+ self._resize_loop_device(path)
|
|
|
+
|
|
|
+ def remove(self, volume):
|
|
|
+ if volume.volume_type in ['read-write', 'volatile']:
|
|
|
+ _remove_if_exists(volume.vid)
|
|
|
+ elif volume.volume_type == 'origin':
|
|
|
+ _remove_if_exists(volume.vid)
|
|
|
+ _remove_if_exists(volume.path_cow)
|
|
|
+
|
|
|
+ def rename(self, volume, old_name, new_name):
|
|
|
+ assert issubclass(volume.__class__, XenVolume)
|
|
|
+ old_dir = os.path.dirname(volume.path)
|
|
|
+ new_dir = os.path.join(os.path.dirname(old_dir), new_name)
|
|
|
+
|
|
|
+ if not os.path.exists(new_dir):
|
|
|
+ os.makedirs(new_dir)
|
|
|
+
|
|
|
+ if volume.volume_type == 'read-write':
|
|
|
+ volume.rename_target_dir(new_name, new_dir)
|
|
|
+ elif volume.volume_type == 'read-only':
|
|
|
+ volume.rename_target_dir(old_name, new_dir)
|
|
|
+ elif volume.volume_type in ['origin', 'volatile']:
|
|
|
+ volume.rename_target_dir(new_dir)
|
|
|
+
|
|
|
+ return volume
|
|
|
+
|
|
|
+ def _resize_loop_device(self, path):
|
|
|
+ # find loop device if any
|
|
|
+ p = subprocess.Popen(
|
|
|
+ ['sudo', 'losetup', '--associated', path],
|
|
|
+ stdout=subprocess.PIPE)
|
|
|
+ result = p.communicate()
|
|
|
+
|
|
|
+ m = re.match(r'^(/dev/loop\d+):\s', result[0])
|
|
|
+ if m is not None:
|
|
|
+ loop_dev = m.group(1)
|
|
|
+
|
|
|
+ # resize loop device
|
|
|
+ subprocess.check_call(['sudo', 'losetup', '--set-capacity',
|
|
|
+ loop_dev])
|
|
|
+
|
|
|
+ def commit_template_changes(self, volume):
|
|
|
+ if volume.volume_type != 'origin':
|
|
|
+ return volume
|
|
|
+
|
|
|
+ if os.path.exists(volume.path_cow):
|
|
|
+ os.rename(volume.path_cow, volume.path_cow + '.old')
|
|
|
+
|
|
|
+ old_umask = os.umask(002)
|
|
|
+ with open(volume.path_cow, 'w') as f_cow:
|
|
|
+ f_cow.truncate(volume.size)
|
|
|
+ os.umask(old_umask)
|
|
|
+ return volume
|
|
|
+
|
|
|
+ def destroy(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+ def setup(self):
|
|
|
+ create_dir_if_not_exists(self.dir_path)
|
|
|
+ appvms_path = os.path.join(self.dir_path, 'appvms')
|
|
|
+ create_dir_if_not_exists(appvms_path)
|
|
|
+ vm_templates_path = os.path.join(self.dir_path, 'vm-templates')
|
|
|
+ create_dir_if_not_exists(vm_templates_path)
|
|
|
+
|
|
|
+ def start(self, volume):
|
|
|
+ if volume.volume_type == 'volatile':
|
|
|
+ self._reset_volume(volume)
|
|
|
+ if volume.volume_type in ['origin', 'snapshot']:
|
|
|
+ _check_path(volume.path_origin)
|
|
|
+ _check_path(volume.path_cow)
|
|
|
+ else:
|
|
|
+ _check_path(volume.path)
|
|
|
+
|
|
|
+ return volume
|
|
|
+
|
|
|
+ def stop(self, volume):
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _reset_volume(self, volume):
|
|
|
+ ''' Remove and recreate a volatile volume '''
|
|
|
+ assert volume.volume_type == 'volatile', "Not a volatile volume"
|
|
|
+
|
|
|
+ assert volume.size
|
|
|
+
|
|
|
+ _remove_if_exists(volume)
|
|
|
+
|
|
|
+ with open(volume.path, "w") as f_volatile:
|
|
|
+ f_volatile.truncate(volume.size)
|
|
|
+ return volume
|
|
|
|
|
|
- root_dev = 'xvda'
|
|
|
- private_dev = 'xvdb'
|
|
|
- volatile_dev = 'xvdc'
|
|
|
- modules_dev = 'xvdd'
|
|
|
+ def target_dir(self, vm):
|
|
|
+ """ Returns the path to vmdir depending on the type of the VM.
|
|
|
|
|
|
- def __init__(self, vm, vmdir, **kwargs):
|
|
|
- """ Instantiate the storage.
|
|
|
+ The default QubesOS file storage saves the vm images in three
|
|
|
+ different directories depending on the ``QubesVM`` type:
|
|
|
+
|
|
|
+ * ``appvms`` for ``QubesAppVm`` or ``QubesHvm``
|
|
|
+ * ``vm-templates`` for ``QubesTemplateVm`` or ``QubesTemplateHvm``
|
|
|
|
|
|
Args:
|
|
|
vm: a QubesVM
|
|
|
- vmdir: the root directory of the pool
|
|
|
+ pool_dir: the root directory of the pool
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ string (str) absolute path to the directory where the vm files
|
|
|
+ are stored
|
|
|
"""
|
|
|
- assert vm is not None
|
|
|
- assert vmdir is not None
|
|
|
+ if vm.is_template():
|
|
|
+ subdir = 'vm-templates'
|
|
|
+ elif vm.is_disposablevm():
|
|
|
+ subdir = 'appvms'
|
|
|
+ return os.path.join(self.dir_path, subdir,
|
|
|
+ vm.template.name + '-dvm')
|
|
|
+ else:
|
|
|
+ subdir = 'appvms'
|
|
|
+
|
|
|
+ return os.path.join(self.dir_path, subdir, vm.name)
|
|
|
+
|
|
|
+ def init_volume(self, vm, volume_config):
|
|
|
+ assert 'volume_type' in volume_config, "Volume type missing " \
|
|
|
+ + str(volume_config)
|
|
|
+ volume_type = volume_config['volume_type']
|
|
|
+ known_types = {
|
|
|
+ 'read-write': ReadWriteFile,
|
|
|
+ 'read-only': ReadOnlyFile,
|
|
|
+ 'origin': OriginFile,
|
|
|
+ 'snapshot': SnapshotFile,
|
|
|
+ 'volatile': VolatileFile,
|
|
|
+ }
|
|
|
+ if volume_type not in known_types:
|
|
|
+ raise StoragePoolException("Unknown volume type " + volume_type)
|
|
|
+
|
|
|
+ if volume_type in ['snapshot', 'read-only']:
|
|
|
+ origin_pool = vm.app.get_pool(volume_config['pool'])
|
|
|
+ assert isinstance(origin_pool,
|
|
|
+ XenPool), 'Origin volume not a xen volume'
|
|
|
+ volume_config['target_dir'] = origin_pool.target_dir(vm.template)
|
|
|
+ name = volume_config['name']
|
|
|
+ volume_config['size'] = vm.template.volume_config[name]['size']
|
|
|
+ else:
|
|
|
+ volume_config['target_dir'] = self.target_dir(vm)
|
|
|
|
|
|
- super(XenStorage, self).__init__(vm, **kwargs)
|
|
|
+ return known_types[volume_type](**volume_config)
|
|
|
|
|
|
- self.vmdir = vmdir
|
|
|
|
|
|
+class XenVolume(Volume):
|
|
|
+ ''' Parent class for the xen volumes implementation which expects a
|
|
|
+ `target_dir` param on initialization.
|
|
|
+ '''
|
|
|
|
|
|
- @property
|
|
|
- def private_img(self):
|
|
|
- '''Path to the private image'''
|
|
|
- return self.abspath(qubes.config.vm_files['private_img'])
|
|
|
+ def __init__(self, target_dir, **kwargs):
|
|
|
+ self.target_dir = target_dir
|
|
|
+ assert self.target_dir, "target_dir not specified"
|
|
|
+ super(XenVolume, self).__init__(**kwargs)
|
|
|
|
|
|
|
|
|
- @property
|
|
|
- def root_img(self):
|
|
|
- '''Path to the root image'''
|
|
|
- return self.vm.template.storage.root_img \
|
|
|
- if hasattr(self.vm, 'template') and self.vm.template \
|
|
|
- else self.abspath(qubes.config.vm_files['root_img'])
|
|
|
+class SizeMixIn(XenVolume):
|
|
|
+ ''' A mix in which expects a `size` param to be > 0 on initialization and
|
|
|
+ provides a usage property wrapper.
|
|
|
+ '''
|
|
|
|
|
|
+ def __init__(self, size=0, **kwargs):
|
|
|
+ assert size, 'Empty size provided'
|
|
|
+ assert size > 0, 'Size for volume ' + kwargs['name'] + ' is <=0'
|
|
|
+ super(SizeMixIn, self).__init__(size=int(size), **kwargs)
|
|
|
|
|
|
@property
|
|
|
- def rootcow_img(self):
|
|
|
- '''Path to the root COW image'''
|
|
|
+ def usage(self):
|
|
|
+ ''' Returns the actualy used space '''
|
|
|
+ return get_disk_usage(self.vid)
|
|
|
|
|
|
- if isinstance(self.vm, qubes.vm.templatevm.TemplateVM):
|
|
|
- return self.abspath(qubes.config.vm_files['rootcow_img'])
|
|
|
+ @property
|
|
|
+ def config(self):
|
|
|
+ ''' return config data for serialization to qubes.xml '''
|
|
|
+ return {'name': self.name,
|
|
|
+ 'pool': self.pool,
|
|
|
+ 'size': str(self.size),
|
|
|
+ 'volume_type': self.volume_type}
|
|
|
+
|
|
|
+
|
|
|
+class ReadWriteFile(SizeMixIn):
|
|
|
+ ''' Represents a readable & writable file image based volume '''
|
|
|
+ def __init__(self, **kwargs):
|
|
|
+ super(ReadWriteFile, self).__init__(**kwargs)
|
|
|
+ self.path = os.path.join(self.target_dir, self.name + '.img')
|
|
|
+ self.vid = self.path
|
|
|
+
|
|
|
+ def rename_target_dir(self, new_name, new_dir):
|
|
|
+ ''' Called by :py:class:`XenPool` when a domain changes it's name '''
|
|
|
+ old_path = self.path
|
|
|
+ file_name = os.path.basename(self.path)
|
|
|
+ new_path = os.path.join(new_dir, file_name)
|
|
|
+
|
|
|
+ os.rename(old_path, new_path)
|
|
|
+ self.target_dir = new_dir
|
|
|
+ self.path = new_path
|
|
|
+ self.vid = self.path
|
|
|
+
|
|
|
+
|
|
|
+class ReadOnlyFile(XenVolume):
|
|
|
+ ''' Represents a readonly file image based volume '''
|
|
|
+ usage = 0
|
|
|
+
|
|
|
+ def __init__(self, size=0, **kwargs):
|
|
|
+ super(ReadOnlyFile, self).__init__(size=int(size), **kwargs)
|
|
|
+ self.path = self.vid
|
|
|
+
|
|
|
+ def rename_target_dir(self, old_name, new_dir):
|
|
|
+ """ Called by :py:class:`XenPool` when a domain changes it's name.
|
|
|
+
|
|
|
+ Only copies the volume if it belongs to the domain being renamed.
|
|
|
+ Currently if a volume is in a directory named the same as the domain,
|
|
|
+ it's ”owned” by the domain.
|
|
|
+ """
|
|
|
+ if os.path.basename(self.target_dir) == old_name:
|
|
|
+ file_name = os.path.basename(self.path)
|
|
|
+ new_path = os.path.join(new_dir, file_name)
|
|
|
+ old_path = self.path
|
|
|
|
|
|
- return None
|
|
|
+ os.rename(old_path, new_path)
|
|
|
|
|
|
+ self.target_dir = new_dir
|
|
|
+ self.path = new_path
|
|
|
+ self.vid = self.path
|
|
|
|
|
|
- @property
|
|
|
- def volatile_img(self):
|
|
|
- '''Path to the volatile image'''
|
|
|
- return self.abspath(qubes.config.vm_files['volatile_img'])
|
|
|
-
|
|
|
-
|
|
|
- def format_disk_dev(self, path, vdev, script=None, rw=True, devtype='disk',
|
|
|
- domain=None):
|
|
|
- if path is None:
|
|
|
- return ''
|
|
|
-
|
|
|
- element = lxml.etree.Element('disk')
|
|
|
- element.set('type', 'block')
|
|
|
- element.set('device', devtype)
|
|
|
-
|
|
|
- element.append(lxml.etree.Element('driver', name='phy'))
|
|
|
- element.append(lxml.etree.Element('source', dev=path))
|
|
|
- element.append(lxml.etree.Element('target', dev=vdev))
|
|
|
-
|
|
|
- if not rw:
|
|
|
- element.append(lxml.etree.Element('readonly'))
|
|
|
- if domain is not None:
|
|
|
- # XXX vm.name?
|
|
|
- element.append(lxml.etree.Element('domain', name=domain))
|
|
|
- if script:
|
|
|
- element.append(lxml.etree.Element('script', path=script))
|
|
|
-
|
|
|
- # TODO return element
|
|
|
- return lxml.etree.tostring(element)
|
|
|
-
|
|
|
-
|
|
|
- def root_dev_config(self):
|
|
|
- if isinstance(self.vm, qubes.vm.templatevm.TemplateVM):
|
|
|
- return self.format_disk_dev(
|
|
|
- '{root}:{rootcow}'.format(
|
|
|
- root=self.root_img,
|
|
|
- rootcow=self.rootcow_img),
|
|
|
- self.root_dev,
|
|
|
- script='block-origin')
|
|
|
-
|
|
|
- elif self.vm.hvm and hasattr(self.vm, 'template'):
|
|
|
- # HVM template-based VM - only one device-mapper layer, in dom0
|
|
|
- # (root+volatile)
|
|
|
- # HVM detection based on 'kernel' property is massive hack,
|
|
|
- # but taken from assumption that VM needs Qubes-specific kernel
|
|
|
- # (actually initramfs) to assemble the second layer of device-mapper
|
|
|
- return self.format_disk_dev(
|
|
|
- '{root}:{volatile}'.format(
|
|
|
- root=self.vm.template.storage.root_img,
|
|
|
- volatile=self.volatile_img),
|
|
|
- self.root_dev,
|
|
|
- script='block-snapshot')
|
|
|
-
|
|
|
- elif hasattr(self.vm, 'template'):
|
|
|
- # any other template-based VM - two device-mapper layers: one
|
|
|
- # in dom0 (here) from root+root-cow, and another one from
|
|
|
- # this+volatile.img
|
|
|
- return self.format_disk_dev(
|
|
|
- '{root}:{template_rootcow}'.format(
|
|
|
- root=self.root_img,
|
|
|
- template_rootcow=self.vm.template.storage.rootcow_img),
|
|
|
- self.root_dev,
|
|
|
- script='block-snapshot',
|
|
|
- rw=False)
|
|
|
|
|
|
- else:
|
|
|
- # standalone qube
|
|
|
- return self.format_disk_dev(self.root_img, self.root_dev)
|
|
|
+class OriginFile(SizeMixIn):
|
|
|
+ ''' Represents a readable, writeable & snapshotable file image based volume.
|
|
|
|
|
|
+ This is used for TemplateVM's
|
|
|
+ '''
|
|
|
|
|
|
- def private_dev_config(self):
|
|
|
- return self.format_disk_dev(self.private_img, self.private_dev)
|
|
|
+ script = 'block-origin'
|
|
|
+
|
|
|
+ def __init__(self, **kwargs):
|
|
|
+ super(OriginFile, self).__init__(**kwargs)
|
|
|
+ self.path_origin = os.path.join(self.target_dir, self.name + '.img')
|
|
|
+ self.path_cow = os.path.join(self.target_dir, self.name + '-cow.img')
|
|
|
+ self.path = '%s:%s' % (self.path_origin, self.path_cow)
|
|
|
+ self.vid = self.path_origin
|
|
|
+
|
|
|
+ def commit(self):
|
|
|
+ ''' Commit Template changes '''
|
|
|
+ raise NotImplementedError
|
|
|
+
|
|
|
+ def rename_target_dir(self, new_dir):
|
|
|
+ ''' Called by :py:class:`XenPool` when a domain changes it's name '''
|
|
|
+ old_path_origin = self.path_origin
|
|
|
+ old_path_cow = self.path_cow
|
|
|
+ new_path_origin = os.path.join(new_dir, self.name + '.img')
|
|
|
+ new_path_cow = os.path.join(new_dir, self.name + '-cow.img')
|
|
|
+ os.rename(old_path_origin, new_path_origin)
|
|
|
+ os.rename(old_path_cow, new_path_cow)
|
|
|
+ self.target_dir = new_dir
|
|
|
+ self.path_origin = new_path_origin
|
|
|
+ self.path_cow = new_path_cow
|
|
|
+ self.path = '%s:%s' % (self.path_origin, self.path_cow)
|
|
|
+ self.vid = self.path_origin
|
|
|
|
|
|
- def volatile_dev_config(self):
|
|
|
- return self.format_disk_dev(self.volatile_img, self.volatile_dev)
|
|
|
+ @property
|
|
|
+ def usage(self):
|
|
|
+ result = 0
|
|
|
+ if os.path.exists(self.path_origin):
|
|
|
+ result += get_disk_usage(self.path_origin)
|
|
|
+ if os.path.exists(self.path_cow):
|
|
|
+ result += get_disk_usage(self.path_cow)
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+class SnapshotFile(XenVolume):
|
|
|
+ ''' Represents a readonly snapshot of an :py:class:`OriginFile` volume '''
|
|
|
+ script = 'block-snapshot'
|
|
|
+ rw = False
|
|
|
+ usage = 0
|
|
|
+
|
|
|
+ def __init__(self, name=None, size=None, **kwargs):
|
|
|
+ assert size
|
|
|
+ super(SnapshotFile, self).__init__(name=name, size=int(size), **kwargs)
|
|
|
+ self.path_origin = os.path.join(self.target_dir, name + '.img')
|
|
|
+ self.path_cow = os.path.join(self.target_dir, name + '-cow.img')
|
|
|
+ self.path = '%s:%s' % (self.path_origin, self.path_cow)
|
|
|
+ self.vid = self.path_origin
|
|
|
+
|
|
|
+
|
|
|
+class VolatileFile(SizeMixIn):
|
|
|
+ ''' Represents a readable & writeable file based volume, which will be
|
|
|
+ discarded and recreated at each startup.
|
|
|
+ '''
|
|
|
+ def __init__(self, **kwargs):
|
|
|
+ super(VolatileFile, self).__init__(**kwargs)
|
|
|
+ self.path = os.path.join(self.target_dir, self.name + '.img')
|
|
|
+ self.vid = self.path
|
|
|
+
|
|
|
+ def rename_target_dir(self, new_dir):
|
|
|
+ ''' Called by :py:class:`XenPool` when a domain changes it's name '''
|
|
|
+ _remove_if_exists(self)
|
|
|
+ file_name = os.path.basename(self.path)
|
|
|
+ self.target_dir = new_dir
|
|
|
+ new_path = os.path.join(new_dir, file_name)
|
|
|
+ self.path = new_path
|
|
|
+ self.vid = self.path
|
|
|
+
|
|
|
+
|
|
|
+def create_sparse_file(path, size):
|
|
|
+ ''' Create an empty sparse file '''
|
|
|
+ if os.path.exists(path):
|
|
|
+ raise IOError("Volume %s already exists", path)
|
|
|
+ parent_dir = os.path.dirname(path)
|
|
|
+ if not os.path.exists(parent_dir):
|
|
|
+ os.makedirs(parent_dir)
|
|
|
+ with open(path, 'a+b') as fh:
|
|
|
+ fh.truncate(size)
|
|
|
+
|
|
|
+
|
|
|
+def get_disk_usage_one(st):
|
|
|
+ '''Extract disk usage of one inode from its stat_result struct.
|
|
|
+
|
|
|
+ If known, get real disk usage, as written to device by filesystem, not
|
|
|
+ logical file size. Those values may be different for sparse files.
|
|
|
+
|
|
|
+ :param os.stat_result st: stat result
|
|
|
+ :returns: disk usage
|
|
|
+ '''
|
|
|
+ try:
|
|
|
+ return st.st_blocks * BLKSIZE
|
|
|
+ except AttributeError:
|
|
|
+ return st.st_size
|
|
|
|
|
|
|
|
|
- def create_on_disk_private_img(self, source_template=None):
|
|
|
- if source_template is None:
|
|
|
- f_private = open(self.private_img, 'a+b')
|
|
|
- f_private.truncate(self.private_img_size)
|
|
|
- f_private.close()
|
|
|
+def get_disk_usage(path):
|
|
|
+ '''Get real disk usage of given path (file or directory).
|
|
|
|
|
|
- else:
|
|
|
- self.vm.log.info("Copying the template's private image: {}".format(
|
|
|
- source_template.storage.private_img))
|
|
|
- self._copy_file(source_template.storage.private_img,
|
|
|
- self.private_img)
|
|
|
+ When *path* points to directory, then it is evaluated recursively.
|
|
|
|
|
|
+ This function tries estiate real disk usage. See documentation of
|
|
|
+ :py:func:`get_disk_usage_one`.
|
|
|
|
|
|
- def create_on_disk_root_img(self, source_template=None):
|
|
|
- if source_template is None:
|
|
|
- fd = open(self.root_img, 'a+b')
|
|
|
- fd.truncate(self.root_img_size)
|
|
|
- fd.close()
|
|
|
+ :param str path: path to evaluate
|
|
|
+ :returns: disk usage
|
|
|
+ '''
|
|
|
+ try:
|
|
|
+ st = os.lstat(path)
|
|
|
+ except OSError:
|
|
|
+ return 0
|
|
|
|
|
|
- elif self.vm.updateable:
|
|
|
- # if not updateable, just use template's disk
|
|
|
- self.vm.log.info("--> Copying the template's root image: {}".format(
|
|
|
- source_template.storage.root_img))
|
|
|
- self._copy_file(source_template.storage.root_img, self.root_img)
|
|
|
+ ret = get_disk_usage_one(st)
|
|
|
|
|
|
+ # if path is not a directory, this is skipped
|
|
|
+ for dirpath, dirnames, filenames in os.walk(path):
|
|
|
+ for name in dirnames + filenames:
|
|
|
+ ret += get_disk_usage_one(os.lstat(os.path.join(dirpath, name)))
|
|
|
|
|
|
- def resize_private_img(self, size):
|
|
|
- fd = open(self.private_img, 'a+b')
|
|
|
- fd.truncate(size)
|
|
|
- fd.close()
|
|
|
+ return ret
|
|
|
|
|
|
- # find loop device if any
|
|
|
- p = subprocess.Popen(
|
|
|
- ['sudo', 'losetup', '--associated', self.private_img],
|
|
|
- stdout=subprocess.PIPE)
|
|
|
- result = p.communicate()
|
|
|
|
|
|
- m = re.match(r'^(/dev/loop\d+):\s', result[0])
|
|
|
- if m is not None:
|
|
|
- loop_dev = m.group(1)
|
|
|
+def create_dir_if_not_exists(path):
|
|
|
+ """ Check if a directory exists in if not create it.
|
|
|
|
|
|
- # resize loop device
|
|
|
- subprocess.check_call(
|
|
|
- ['sudo', 'losetup', '--set-capacity', loop_dev])
|
|
|
+ This method does not create any parent directories.
|
|
|
+ """
|
|
|
+ if not os.path.exists(path):
|
|
|
+ os.mkdir(path)
|
|
|
|
|
|
|
|
|
- def commit_template_changes(self):
|
|
|
- assert isinstance(self.vm, qubes.vm.templatevm.TemplateVM)
|
|
|
+def copy_file(source, destination):
|
|
|
+ '''Effective file copy, preserving sparse files etc.
|
|
|
+ '''
|
|
|
+ # TODO: Windows support
|
|
|
+ # We prefer to use Linux's cp, because it nicely handles sparse files
|
|
|
+ assert os.path.exists(source), \
|
|
|
+ "Missing the source %s to copy from" % source
|
|
|
+ assert not os.path.exists(destination), \
|
|
|
+ "Destination %s already exists" % destination
|
|
|
|
|
|
- # TODO: move rootcow_img to this class; the same for vm.is_outdated()
|
|
|
- if os.path.exists(self.vm.rootcow_img):
|
|
|
- os.rename(self.vm.rootcow_img, self.vm.rootcow_img + '.old')
|
|
|
+ parent_dir = os.path.dirname(destination)
|
|
|
+ if not os.path.exists(parent_dir):
|
|
|
+ os.makedirs(parent_dir)
|
|
|
|
|
|
- old_umask = os.umask(002)
|
|
|
- f_cow = open(self.vm.rootcow_img, 'w')
|
|
|
- f_root = open(self.root_img, 'r')
|
|
|
- f_root.seek(0, os.SEEK_END)
|
|
|
- # make empty sparse file of the same size as root.img
|
|
|
- f_cow.truncate(f_root.tell())
|
|
|
- f_cow.close()
|
|
|
- f_root.close()
|
|
|
- os.umask(old_umask)
|
|
|
+ try:
|
|
|
+ subprocess.check_call(['cp', '--reflink=auto', source, destination])
|
|
|
+ except subprocess.CalledProcessError:
|
|
|
+ raise IOError('Error while copying {!r} to {!r}'.format(source,
|
|
|
+ destination))
|
|
|
+
|
|
|
+
|
|
|
+def _remove_if_exists(volume):
|
|
|
+ if os.path.exists(volume.path):
|
|
|
+ os.remove(volume.path)
|
|
|
|
|
|
|
|
|
- def reset_volatile_storage(self):
|
|
|
- try:
|
|
|
- # no template set, in any way (Standalone VM, Template VM)
|
|
|
- if self.vm.template is None:
|
|
|
- raise AttributeError
|
|
|
-
|
|
|
- # template-based HVM with only one device-mapper layer -
|
|
|
- # volatile.img used as upper layer on root.img, no root-cow.img
|
|
|
- # intermediate layer
|
|
|
- if self.vm.hvm:
|
|
|
- if os.path.exists(self.volatile_img):
|
|
|
- if self.vm.debug:
|
|
|
- if os.path.getmtime(self.vm.template.storage.root_img) \
|
|
|
- > os.path.getmtime(self.volatile_img):
|
|
|
- self.vm.log.warning(
|
|
|
- 'Template have changed, resetting root.img')
|
|
|
- else:
|
|
|
- self.vm.log.warning(
|
|
|
- 'Debug mode: not resetting root.img; if you'
|
|
|
- ' want to force root.img reset, either'
|
|
|
- ' update template VM, or remove volatile.img'
|
|
|
- ' file.')
|
|
|
- return
|
|
|
-
|
|
|
- os.remove(self.volatile_img)
|
|
|
-
|
|
|
- # FIXME stat on f_root; with open() ...
|
|
|
- f_volatile = open(self.volatile_img, "w")
|
|
|
- f_root = open(self.vm.template.storage.root_img, "r")
|
|
|
- # make empty sparse file of the same size as root.img
|
|
|
- f_root.seek(0, os.SEEK_END)
|
|
|
- f_volatile.truncate(f_root.tell())
|
|
|
- f_volatile.close()
|
|
|
- f_root.close()
|
|
|
- return # XXX why is that? super() does not run
|
|
|
- except AttributeError: # self.vm.template
|
|
|
- pass
|
|
|
-
|
|
|
- super(XenStorage, self).reset_volatile_storage()
|
|
|
-
|
|
|
-
|
|
|
- def prepare_for_vm_startup(self):
|
|
|
- super(XenStorage, self).prepare_for_vm_startup()
|
|
|
-
|
|
|
- if self.drive is not None:
|
|
|
- # pylint: disable=unused-variable
|
|
|
- (drive_type, drive_domain, drive_path) = self.drive.split(":")
|
|
|
-
|
|
|
- if drive_domain.lower() != "dom0":
|
|
|
- # XXX "VM '{}' holding '{}' does not exists".format(
|
|
|
- drive_vm = self.vm.app.domains[drive_domain]
|
|
|
-
|
|
|
- if not drive_vm.is_running():
|
|
|
- raise qubes.exc.QubesVMNotRunningError(drive_vm,
|
|
|
- 'VM {!r} holding {!r} isn\'t running'.format(
|
|
|
- drive_domain, drive_path))
|
|
|
-
|
|
|
- if self.rootcow_img and not os.path.exists(self.rootcow_img):
|
|
|
- self.commit_template_changes()
|
|
|
-
|
|
|
-class XenPool(qubes.storage.Pool):
|
|
|
- def get_storage(self):
|
|
|
- """ Returns an instantiated ``XenStorage``. """
|
|
|
- return XenStorage(self.vm, vmdir=self.vmdir)
|
|
|
+def _check_path(path):
|
|
|
+ ''' Raise an StoragePoolException if ``path`` does not exist'''
|
|
|
+ if not os.path.exists(path):
|
|
|
+ raise StoragePoolException('Missing image file: %s' % path)
|