core-admin/qubes/storage/file.py

510 lines
18 KiB
Python
Raw Normal View History

2015-01-16 15:33:03 +01:00
#
2015-01-19 18:03:23 +01:00
# The Qubes OS Project, https://www.qubes-os.org/
2015-01-16 15:33:03 +01:00
#
2015-01-19 18:03:23 +01:00
# Copyright (C) 2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
# Copyright (C) 2013-2015 Marek Marczykowski-Górecki
# <marmarek@invisiblethingslab.com>
2015-01-16 15:33:03 +01:00
# Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
#
2015-01-19 18:03:23 +01:00
# This program 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.
2015-01-16 15:33:03 +01:00
#
# This program 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.
#
2015-01-19 18:03:23 +01:00
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
2015-01-16 15:33:03 +01:00
#
2016-06-16 13:32:54 +02:00
''' This module contains pool implementations backed by file images'''
2015-01-16 15:33:03 +01:00
from __future__ import absolute_import
import os
import os.path
2015-01-19 18:14:15 +01:00
import re
2015-01-16 15:33:03 +01:00
import subprocess
2016-07-12 17:53:31 +02:00
import qubes.storage
2015-01-16 15:33:03 +01:00
BLKSIZE = 512
2015-01-16 15:33:03 +01:00
2016-07-12 17:53:31 +02:00
class FilePool(qubes.storage.Pool):
''' File based 'original' disk implementation
''' # pylint: disable=protected-access
driver = 'file'
2015-01-16 15:33:03 +01:00
2016-07-12 17:53:31 +02:00
def __init__(self, revisions_to_keep=1, dir_path=None, **kwargs):
super(FilePool, self).__init__(revisions_to_keep=revisions_to_keep,
**kwargs)
2016-04-15 20:15:18 +02:00
assert dir_path, "No pool dir_path specified"
self.dir_path = os.path.normpath(dir_path)
self._volumes = []
2016-04-15 16:01:27 +02:00
@property
def config(self):
return {
'name': self.name,
'dir_path': self.dir_path,
'driver': FilePool.driver,
2016-07-12 17:53:31 +02:00
'revisions_to_keep': self.revisions_to_keep
2016-04-15 16:01:27 +02:00
}
2016-07-12 17:53:31 +02:00
def clone(self, source, target):
new_dir = os.path.dirname(target.path)
if target._is_origin or target._is_volume:
if not os.path.exists:
os.makedirs(new_dir)
copy_file(source.path, target.path)
return target
def create(self, volume):
assert isinstance(volume.size, int) and volume.size > 0, \
2016-07-12 17:53:31 +02:00
'Volatile volume size must be > 0'
if volume._is_origin:
create_sparse_file(volume.path, volume.size)
create_sparse_file(volume.path_cow, volume.size)
elif not volume._is_snapshot:
if volume.source is not None:
source_path = os.path.join(self.dir_path,
volume.source + '.img')
copy_file(source_path, volume.path)
elif volume._is_volatile:
pass
else:
create_sparse_file(volume.path, volume.size)
def init_volume(self, vm, volume_config):
volume_config['dir_path'] = self.dir_path
if os.path.join(self.dir_path, self._vid_prefix(vm)) == vm.dir_path:
volume_config['backward_comp'] = True
if 'vid' not in volume_config:
volume_config['vid'] = os.path.join(
self._vid_prefix(vm), volume_config['name'])
try:
if volume_config['reset_on_start']:
volume_config['revisions_to_keep'] = 0
except KeyError:
pass
finally:
if 'revisions_to_keep' not in volume_config:
volume_config['revisions_to_keep'] = self.revisions_to_keep
volume = FileVolume(**volume_config)
self._volumes += [volume]
return volume
def is_dirty(self, volume):
return False # TODO: How to implement this?
2016-06-21 14:21:31 +02:00
def resize(self, volume, size):
''' Expands volume, throws
2016-07-12 17:53:31 +02:00
:py:class:`qubst.storage.qubes.storage.StoragePoolException` if
given size is less than current_size
2016-06-21 12:47:47 +02:00
''' # pylint: disable=no-self-use
2016-07-12 17:53:31 +02:00
if not volume.rw:
msg = 'Can not resize reađonly volume {!s}'.format(volume)
raise qubes.storage.StoragePoolException(msg)
if size <= volume.size:
2016-07-12 17:53:31 +02:00
raise qubes.storage.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))
2016-07-12 17:53:31 +02:00
with open(volume.path, 'a+b') as fd:
fd.truncate(size)
2015-01-16 15:33:03 +01:00
2016-07-12 17:53:31 +02:00
p = subprocess.Popen(['sudo', 'losetup', '--associated', volume.path],
stdout=subprocess.PIPE)
result = p.communicate()
2017-01-26 18:26:06 +01:00
m = re.match(r'^(/dev/loop\d+):\s', result[0].decode())
if m is not None:
loop_dev = m.group(1)
# resize loop device
2016-07-12 17:53:31 +02:00
subprocess.check_call(['sudo', 'losetup', '--set-capacity',
loop_dev])
2016-08-28 19:51:33 +02:00
volume.size = size
2016-04-05 20:04:20 +02:00
2016-04-15 17:10:55 +02:00
def remove(self, volume):
2016-07-12 17:53:31 +02:00
if not volume.internal:
return # do not remove random attached file volumes
elif volume._is_snapshot:
return # no need to remove, because it's just a snapshot
else:
_remove_if_exists(volume.path)
2016-07-12 17:53:31 +02:00
if volume._is_origin:
_remove_if_exists(volume.path_cow)
2016-04-15 17:10:55 +02:00
def rename(self, volume, old_name, new_name):
assert issubclass(volume.__class__, FileVolume)
2016-07-12 17:53:31 +02:00
subdir, _, volume_path = volume.vid.split('/', 2)
if volume._is_origin:
# TODO: Renaming the old revisions
new_path = os.path.join(self.dir_path, subdir, new_name)
if not os.path.exists(new_path):
os.mkdir(new_path, 0o755)
2016-07-12 17:53:31 +02:00
new_volume_path = os.path.join(new_path, self.name + '.img')
if not volume.backward_comp:
os.rename(volume.path, new_volume_path)
new_volume_path_cow = os.path.join(new_path, self.name + '-cow.img')
if os.path.exists(new_volume_path_cow) and not volume.backward_comp:
os.rename(volume.path_cow, new_volume_path_cow)
volume.vid = os.path.join(subdir, new_name, volume_path)
2016-07-12 17:53:31 +02:00
return volume
2016-07-12 17:53:31 +02:00
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
msg = "Can not import snapshot volume {!s} in to pool {!s} "
msg = msg.format(src_volume, self)
assert not src_volume.snap_on_start, msg
if dst_volume.save_on_stop:
copy_file(src_pool.export(src_volume), dst_volume.path)
return dst_volume
2016-07-12 17:53:31 +02:00
def commit(self, volume):
msg = 'Tried to commit a non commitable volume {!r}'.format(volume)
assert (volume._is_origin or volume._is_volume) and volume.rw, msg
2016-07-12 17:53:31 +02:00
if volume._is_volume:
return volume
2015-01-16 15:33:03 +01:00
if os.path.exists(volume.path_cow):
2016-07-12 17:53:31 +02:00
old_path = volume.path_cow + '.old'
os.rename(volume.path_cow, old_path)
2015-01-16 15:33:03 +01:00
2016-07-21 23:16:39 +02:00
old_umask = os.umask(0o002)
with open(volume.path_cow, 'w') as f_cow:
f_cow.truncate(volume.size)
2015-01-16 15:33:03 +01:00
os.umask(old_umask)
2016-04-01 13:44:29 +02:00
return volume
2016-04-15 20:33:04 +02:00
def destroy(self):
pass
2016-07-12 17:53:31 +02:00
def export(self, volume):
return volume.path
def reset(self, volume):
''' Remove and recreate a volatile volume '''
assert volume._is_volatile, "Not a volatile volume"
assert isinstance(volume.size, int) and volume.size > 0, \
2016-07-12 17:53:31 +02:00
'Volatile volume size must be > 0'
_remove_if_exists(volume.path)
with open(volume.path, "w") as f_volatile:
f_volatile.truncate(volume.size)
return volume
def revert(self, volume, revision=None):
if revision is not None:
try:
return volume.revisions[revision]
except KeyError:
msg = "Volume {!r} does not have revision {!s}"
msg = msg.format(volume, revision)
raise qubes.storage.StoragePoolException(msg)
else:
try:
old_path = volume.revisions.values().pop()
os.rename(old_path, volume.path_cow)
except IndexError:
msg = "Volume {!r} does not have old revisions".format(volume)
raise qubes.storage.StoragePoolException(msg)
2016-04-15 20:33:04 +02:00
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)
2016-04-01 13:44:29 +02:00
def start(self, volume):
if volume._is_volatile:
2016-07-12 17:53:31 +02:00
self.reset(volume)
else:
_check_path(volume.path)
if volume.snap_on_start:
if not volume.save_on_stop:
# make sure previous snapshot is removed - even if VM
# shutdown routing wasn't called (power interrupt or so)
_remove_if_exists(volume.path_cow)
try:
_check_path(volume.path_cow)
except qubes.storage.StoragePoolException:
create_sparse_file(volume.path_cow, volume.size)
_check_path(volume.path_cow)
if hasattr(volume, 'path_source_cow'):
try:
_check_path(volume.path_source_cow)
except qubes.storage.StoragePoolException:
create_sparse_file(volume.path_source_cow, volume.size)
_check_path(volume.path_source_cow)
2016-04-01 13:44:29 +02:00
return volume
def stop(self, volume):
2016-07-12 17:53:31 +02:00
if volume.save_on_stop:
self.commit(volume)
elif volume.snap_on_start:
_remove_if_exists(volume.path_cow)
else:
2016-07-12 17:53:31 +02:00
_remove_if_exists(volume.path)
return volume
@staticmethod
def _vid_prefix(vm):
''' Helper to create a prefix for the vid for volume
''' # FIX Remove this if we drop the file backend
import qubes.vm.templatevm # pylint: disable=redefined-outer-name
import qubes.vm.dispvm # pylint: disable=redefined-outer-name
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
subdir = 'vm-templates'
else:
subdir = 'appvms'
return os.path.join(subdir, vm.name)
def target_dir(self, vm):
2016-04-15 20:15:18 +02:00
""" Returns the path to vmdir depending on the type of the VM.
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
pool_dir: the root directory of the pool
Returns:
string (str) absolute path to the directory where the vm files
are stored
"""
2016-04-15 20:40:48 +02:00
2016-07-12 17:53:31 +02:00
return os.path.join(self.dir_path, self._vid_prefix(vm))
2016-04-14 19:00:52 +02:00
def verify(self, volume):
return volume.verify()
@property
def volumes(self):
return self._volumes
2016-04-14 19:00:52 +02:00
2016-04-15 19:59:40 +02:00
2016-07-12 17:53:31 +02:00
class FileVolume(qubes.storage.Volume):
''' Parent class for the xen volumes implementation which expects a
2016-07-12 17:53:31 +02:00
`target_dir` param on initialization. '''
2016-07-12 17:53:31 +02:00
def __init__(self, dir_path, backward_comp=False, **kwargs):
self.dir_path = dir_path
self.backward_comp = backward_comp
assert self.dir_path, "dir_path not specified"
super(FileVolume, self).__init__(**kwargs)
2016-07-12 17:53:31 +02:00
if self.snap_on_start and self.source is None:
msg = "snap_on_start specified on {!r} but no volume source set"
msg = msg.format(self.name)
raise qubes.storage.StoragePoolException(msg)
elif not self.snap_on_start and self.source is not None:
msg = "source specified on {!r} but no snap_on_start set"
msg = msg.format(self.name)
raise qubes.storage.StoragePoolException(msg)
if self._is_snapshot:
self.path = os.path.join(self.dir_path, self.source + '.img')
img_name = self.source + '-cow.img'
self.path_source_cow = os.path.join(self.dir_path, img_name)
img_name = self.vid + '-cow.img'
2016-07-12 17:53:31 +02:00
self.path_cow = os.path.join(self.dir_path, img_name)
elif self._is_volume or self._is_volatile:
self.path = os.path.join(self.dir_path, self.vid + '.img')
elif self._is_origin:
self.path = os.path.join(self.dir_path, self.vid + '.img')
img_name = self.vid + '-cow.img'
self.path_cow = os.path.join(self.dir_path, img_name)
else:
assert False, 'This should not happen'
def verify(self):
''' Verifies the volume. '''
if not os.path.exists(self.path) and not self._is_volatile:
msg = 'Missing image file: {!s}.'.format(self.path)
raise qubes.storage.StoragePoolException(msg)
return True
@property
def script(self):
if self._is_volume or self._is_volatile:
return None
elif self._is_origin:
return 'block-origin'
elif self._is_origin_snapshot or self._is_snapshot:
return 'block-snapshot'
def block_device(self):
''' Return :py:class:`qubes.storage.BlockDevice` for serialization in
2016-07-12 17:53:31 +02:00
the libvirt XML template as <disk>.
2016-06-16 13:32:54 +02:00
'''
2016-07-12 17:53:31 +02:00
path = self.path
if self._is_snapshot:
path += ":" + self.path_source_cow
2016-07-12 17:53:31 +02:00
if self._is_origin or self._is_snapshot:
path += ":" + self.path_cow
return qubes.storage.BlockDevice(path, self.name, self.script, self.rw,
2016-07-12 17:53:31 +02:00
self.domain, self.devtype)
2016-06-16 13:32:54 +02:00
2016-07-12 17:53:31 +02:00
@property
def revisions(self):
if not hasattr(self, 'path_cow'):
return {}
2016-04-15 19:59:40 +02:00
2016-07-12 17:53:31 +02:00
old_revision = self.path_cow + '.old' # pylint: disable=no-member
2016-07-12 17:53:31 +02:00
if not os.path.exists(old_revision):
return {}
else:
seconds = os.path.getctime(old_revision)
iso_date = qubes.storage.isodate(seconds).split('.', 1)[0]
return {iso_date: old_revision}
2016-04-15 19:59:40 +02:00
@property
def usage(self):
''' Returns the actualy used space '''
return get_disk_usage(self.vid)
2016-04-15 19:57:23 +02:00
@property
2016-07-12 17:53:31 +02:00
def _is_volatile(self):
''' Internal helper. Useful for differentiating volume handling '''
return not self.snap_on_start and not self.save_on_stop
2016-04-15 19:59:40 +02:00
@property
2016-07-12 17:53:31 +02:00
def _is_origin(self):
''' Internal helper. Useful for differentiating volume handling '''
# pylint: disable=line-too-long
return self.save_on_stop and self.revisions_to_keep > 0 # NOQA
2016-07-12 17:53:31 +02:00
@property
def _is_snapshot(self):
''' Internal helper. Useful for differentiating volume handling '''
return self.snap_on_start and not self.save_on_stop
2016-04-15 19:59:40 +02:00
2016-07-12 17:53:31 +02:00
@property
def _is_origin_snapshot(self):
''' Internal helper. Useful for differentiating volume handling '''
return self.snap_on_start and self.save_on_stop
2016-07-12 17:53:31 +02:00
@property
def _is_volume(self):
''' Internal helper. Usefull for differentiating volume handling '''
# pylint: disable=line-too-long
return not self.snap_on_start and self.save_on_stop and self.revisions_to_keep == 0 # NOQA
2016-06-21 14:41:14 +02:00
def create_sparse_file(path, size):
''' Create an empty sparse file '''
2016-04-15 19:59:40 +02:00
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)
2016-04-15 19:59:40 +02:00
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 get_disk_usage(path):
'''Get real disk usage of given path (file or directory).
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`.
:param str path: path to evaluate
:returns: disk usage
'''
try:
st = os.lstat(path)
except OSError:
return 0
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)))
return ret
def create_dir_if_not_exists(path):
""" Check if a directory exists in if not create it.
This method does not create any parent directories.
"""
if not os.path.exists(path):
os.mkdir(path)
def copy_file(source, destination):
2016-06-16 13:37:17 +02:00
'''Effective file copy, preserving sparse files etc.'''
# 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
parent_dir = os.path.dirname(destination)
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
try:
cmd = ['cp', '--sparse=auto',
2016-07-12 17:53:31 +02:00
'--reflink=auto', source, destination]
subprocess.check_call(cmd)
except subprocess.CalledProcessError:
raise IOError('Error while copying {!r} to {!r}'.format(source,
destination))
def _remove_if_exists(path):
2016-06-21 12:47:47 +02:00
''' Removes a file if it exist, silently succeeds if file does not exist '''
if os.path.exists(path):
os.remove(path)
def _check_path(path):
''' Raise an StoragePoolException if ``path`` does not exist'''
if not os.path.exists(path):
2016-07-12 17:53:31 +02:00
msg = 'Missing image file: %s' % path
raise qubes.storage.StoragePoolException(msg)