Merge branch 'core3-storage3'
This commit is contained in:
		
						commit
						dddd94b339
					
				| @ -16,6 +16,7 @@ manpages and API documentation. For primary user documentation, see | |||||||
|    qubes |    qubes | ||||||
|    qubes-vm/index |    qubes-vm/index | ||||||
|    qubes-events |    qubes-events | ||||||
|  |    qubes-storage | ||||||
|    qubes-exc |    qubes-exc | ||||||
|    qubes-ext |    qubes-ext | ||||||
|    qubes-log |    qubes-log | ||||||
|  | |||||||
							
								
								
									
										145
									
								
								doc/qubes-storage.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								doc/qubes-storage.rst
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | :py:mod:`qubes.storage` -- Qubes data storage | ||||||
|  | ============================================= | ||||||
|  | 
 | ||||||
|  | Qubes provide extensible API for domains data storage. Each domain have | ||||||
|  | multiple storage volumes, for different purposes. Each volume is provided by | ||||||
|  | some storage pool. Qubes support different storage pool drivers, and it's | ||||||
|  | possible to register additional 3rd-party drivers. | ||||||
|  | 
 | ||||||
|  | Domain's storage volumes: | ||||||
|  | 
 | ||||||
|  |  - `root` - this is where operating system is installed. The volume is | ||||||
|  |    available read-write to :py:class:`~qubes.vm.templatevm.TemplateVM` and | ||||||
|  |    :py:class:`~qubes.vm.standalonevm.StandaloneVM`, and read-only to others | ||||||
|  |    (:py:class:`~qubes.vm.appvm.AppVM` and :py:class:`~qubes.vm.dispvm.DispVM`). | ||||||
|  |  - `private` - this is where domain's data live. The volume is available | ||||||
|  |    read-write to all domain classes (including :py:class:`~qubes.vm.dispvm.DispVM`, | ||||||
|  |    but data written there is discarded on domain shutdown). | ||||||
|  |  - `volatile` - this is used for any data that do not to persist. This include | ||||||
|  |    swap, copy-on-write layer for `root` volume etc. | ||||||
|  |  - `kernel` - domain boot files - operating system kernel, initial ramdisk, | ||||||
|  |    kernel modules etc. This volume is provided read-only and should be provided by | ||||||
|  |    a storage pool respecting :py:attr:`qubes.vm.qubesvm.QubesVM.kernel` property. | ||||||
|  | 
 | ||||||
|  | Storage pool concept | ||||||
|  | -------------------- | ||||||
|  | 
 | ||||||
|  | Storage pool is responsible for managing its volumes. Qubes have defined | ||||||
|  | storage pool driver API, allowing to put domains storage in various places. By | ||||||
|  | default two drivers are provided: :py:class:`qubes.storage.file.FilePool` | ||||||
|  | (named `file`) and :py:class:`qubes.storage.lvm.ThinPool` (named `lvm_thin`). | ||||||
|  | But the API allow to implement variety of other drivers (like additionally | ||||||
|  | encrypted storage, external disk, drivers using special features of some | ||||||
|  | filesystems like btrfs, etc). | ||||||
|  | 
 | ||||||
|  | Most of storage API focus on storage volumes. Each volume have at least those | ||||||
|  | properties: | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.rw` - should the volume be available | ||||||
|  |    read-only or read-write to the domain | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.snap_on_start` - should the domain start | ||||||
|  |    with its own state of the volume, or rather a snapshot of its template volume | ||||||
|  |    (pointed by a :py:attr:`~qubes.storage.Volume.source` property). This can be | ||||||
|  |    set to `True` only if a domain do have `template` property (AppVM and DispVM). | ||||||
|  |    If the domain's template is running already, the snapshot should be made out of | ||||||
|  |    the template's before its startup. | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.save_on_stop` - should the volume state be | ||||||
|  |    saved or discarded on domain | ||||||
|  |    stop. In either case, while the domain is running, volume's current state | ||||||
|  |    should not be committed immediately. This is to allow creating snapshots of the | ||||||
|  |    volume's state from before domain start (see | ||||||
|  |    :py:attr:`~qubes.storage.Volume.snap_on_start`). | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.revisions_to_keep` - number of volume | ||||||
|  |    revisions to keep. If greater than zero, at each domain stop (and if | ||||||
|  |    :py:attr:`~qubes.storage.Volume.save_on_stop` is `True`) new revision is saved | ||||||
|  |    and old ones exceeding :py:attr:`~qubes.storage.Volume.revisions_to_keep` limit | ||||||
|  |    are removed. | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.source` - source volume for | ||||||
|  |    :py:attr:`~qubes.storage.Volume.snap_on_start` volumes | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.vid` - pool specific volume identifier, must | ||||||
|  |    be unique inside given pool | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.pool` - storage pool object owning this volume | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.name` - name of the volume inside owning | ||||||
|  |    domain (like `root`, or `private`) | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.size` - size of the volume, in bytes | ||||||
|  | 
 | ||||||
|  | Storage pool driver may define additional properties. | ||||||
|  | 
 | ||||||
|  | Storage pool driver API | ||||||
|  | ----------------------- | ||||||
|  | 
 | ||||||
|  | Storage pool driver need to implement two classes: | ||||||
|  |  - pool class - inheriting from :py:class:`qubes.storage.Pool` | ||||||
|  |  - volume class - inheriting from :py:class:`qubes.storage.Volume` | ||||||
|  | 
 | ||||||
|  | Pool class should be registered with `qubes.storage` entry_point, under the | ||||||
|  | name of storage pool driver. Volume class instances should be returned by | ||||||
|  | :py:meth:`qubes.storage.Pool.init_volume` method of pool class instance. | ||||||
|  | 
 | ||||||
|  | Methods required to be implemented by the pool class: | ||||||
|  |  - :py:meth:`~qubes.storage.Pool.init_volume` - return instance of appropriate | ||||||
|  |    volume class; this method should not alter any persistent disk state, it is | ||||||
|  |    used to instantiate both existing volumes and create new ones | ||||||
|  |  - :py:meth:`~qubes.storage.Pool.setup` - setup new storage pool | ||||||
|  |  - :py:meth:`~qubes.storage.Pool.destroy` - destroy storage pool | ||||||
|  | 
 | ||||||
|  | Methods and properties required to be implemented by the volume class: | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.create` - create volume on disk | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.remove` - remove volume from disk | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.start` - prepare the volume for domain start; | ||||||
|  |    this include making a snapshot if | ||||||
|  |    :py:attr:`~qubes.storage.Volume.snap_on_start` is `True` | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.stop` - cleanup after domain shutdown; this | ||||||
|  |    include committing changes to the volume if | ||||||
|  |    :py:attr:`~qubes.storage.Volume.save_on_stop` is `True` | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.export` - return a path to be read to extract | ||||||
|  |    volume data; for complex formats, this can be a pipe (connected to some | ||||||
|  |    data-extracting process) | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.import_data` - return a path the data should | ||||||
|  |    be written to, to import volume data; for complex formats, this can be pipe | ||||||
|  |    (connected to some data-importing process) | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.import_data_end` - finish data import | ||||||
|  |    operation (cleanup temporary files etc); this methods is called always after | ||||||
|  |    :py:meth:`~qubes.storage.Volume.import_data` regardless if operation was | ||||||
|  |    successful or not | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.import_volume` - import data from another volume | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.resize` - resize volume | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.revert` - revert volume state to a given revision | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.revisions` - collection of volume revisions (to use | ||||||
|  |    with :py:meth:`qubes.storage.Volume.revert`) | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.is_dirty` - is volume properly committed | ||||||
|  |    after domain shutdown? Applies only to volumes with | ||||||
|  |    :py:attr:`~qubes.storage.Volume.save_on_stop` set to `True` | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.is_outdated` - have the source volume started | ||||||
|  |    since domain startup? applies only to volumes with | ||||||
|  |    :py:attr:`~qubes.storage.Volume.snap_on_start` set to `True` | ||||||
|  |  - :py:attr:`~qubes.storage.Volume.config` - volume configuration, this should | ||||||
|  |    be enough to later reinstantiate the same volume object | ||||||
|  |  - :py:meth:`~qubes.storage.Volume.block_device` - return | ||||||
|  |    :py:class:`qubes.storage.BlockDevice` instance required to configure volume in | ||||||
|  |    libvirt | ||||||
|  | 
 | ||||||
|  | Some storage pool drivers can provide limited functionality only - for example | ||||||
|  | support only `volatile` volumes (those with | ||||||
|  | :py:attr:`~qubes.storage.Volume.snap_on_start` is `False`, | ||||||
|  | :py:attr:`~qubes.storage.Volume.save_on_stop` is `False`, and | ||||||
|  | :py:attr:`~qubes.storage.Volume.rw` is `True`). In that case, it should raise | ||||||
|  | :py:exc:`NotImplementedError` in :py:meth:`qubes.storage.Pool.init_volume` when | ||||||
|  | trying to instantiate unsupported volume. | ||||||
|  | 
 | ||||||
|  | Note that pool driver should be prepared to recover from power loss before | ||||||
|  | stopping a domain - so, if volume have | ||||||
|  | :py:attr:`~qubes.storage.Volume.save_on_stop` is `True`, and | ||||||
|  | :py:meth:`qubes.storage.Volume.stop` wasn't called, next | ||||||
|  | :py:meth:`~qubes.storage.Volume.start` should pick up previous (not committed) | ||||||
|  | state. | ||||||
|  | 
 | ||||||
|  | See specific methods documentation for details. | ||||||
|  | 
 | ||||||
|  | Module contents | ||||||
|  | --------------- | ||||||
|  | 
 | ||||||
|  | .. automodule:: qubes.storage | ||||||
|  |    :members: | ||||||
|  |    :show-inheritance: | ||||||
|  | 
 | ||||||
|  | .. vim: ts=3 sw=3 et | ||||||
| @ -275,7 +275,7 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): | |||||||
|         volume = self.dest.volumes[self.arg] |         volume = self.dest.volumes[self.arg] | ||||||
|         # properties defined in API |         # properties defined in API | ||||||
|         volume_properties = [ |         volume_properties = [ | ||||||
|             'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source', |             'pool', 'vid', 'size', 'usage', 'rw', 'source', | ||||||
|             'save_on_stop', 'snap_on_start'] |             'save_on_stop', 'snap_on_start'] | ||||||
|         return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in |         return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in | ||||||
|             volume_properties) |             volume_properties) | ||||||
|  | |||||||
							
								
								
									
										66
									
								
								qubes/app.py
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								qubes/app.py
									
									
									
									
									
								
							| @ -28,6 +28,7 @@ import grp | |||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
|  | import subprocess | ||||||
| import sys | import sys | ||||||
| import tempfile | import tempfile | ||||||
| import time | import time | ||||||
| @ -548,6 +549,46 @@ class VMCollection(object): | |||||||
|             'https://xkcd.com/221/', |             'https://xkcd.com/221/', | ||||||
|             'http://dilbert.com/strip/2001-10-25')[random.randint(0, 1)]) |             'http://dilbert.com/strip/2001-10-25')[random.randint(0, 1)]) | ||||||
| 
 | 
 | ||||||
|  | def _default_pool(app): | ||||||
|  |     ''' Default storage pool. | ||||||
|  | 
 | ||||||
|  |     1. If there is one named 'default', use it. | ||||||
|  |     2. Check if root fs is on LVM thin - use that | ||||||
|  |     3. Look for file-based pool pointing /var/lib/qubes | ||||||
|  |     4. Fail | ||||||
|  |     ''' | ||||||
|  |     if 'default' in app.pools: | ||||||
|  |         return app.pools['default'] | ||||||
|  |     else: | ||||||
|  |         rootfs = os.stat('/') | ||||||
|  |         root_major = (rootfs.st_dev & 0xff00) >> 8 | ||||||
|  |         root_minor = rootfs.st_dev & 0xff | ||||||
|  |         for pool in app.pools.values(): | ||||||
|  |             if pool.config.get('driver', None) != 'lvm_thin': | ||||||
|  |                 continue | ||||||
|  |             thin_pool = pool.config['thin_pool'] | ||||||
|  |             thin_volumes = subprocess.check_output( | ||||||
|  |                 ['lvs', '--select', 'pool_lv=' + thin_pool, | ||||||
|  |                 '-o', 'lv_kernel_major,lv_kernel_minor', '--noheadings']) | ||||||
|  |             if any((str(root_major), str(root_minor)) == thin_vol.split() | ||||||
|  |                     for thin_vol in thin_volumes.splitlines()): | ||||||
|  |                 return pool | ||||||
|  |         # not a thin volume? look for file pools | ||||||
|  |         for pool in app.pools.values(): | ||||||
|  |             if pool.config.get('driver', None) != 'file': | ||||||
|  |                 continue | ||||||
|  |             if pool.config['dir_path'] == '/var/lib/qubes': | ||||||
|  |                 return pool | ||||||
|  |         raise AttributeError('Cannot determine default storage pool') | ||||||
|  | 
 | ||||||
|  | def _setter_pool(app, prop, value): | ||||||
|  |     if isinstance(value, qubes.storage.Pool): | ||||||
|  |         return value | ||||||
|  |     try: | ||||||
|  |         return app.pools[value] | ||||||
|  |     except KeyError: | ||||||
|  |         raise qubes.exc.QubesPropertyValueError(app, prop, value, | ||||||
|  |             'No such storage pool') | ||||||
| 
 | 
 | ||||||
| class Qubes(qubes.PropertyHolder): | class Qubes(qubes.PropertyHolder): | ||||||
|     '''Main Qubes application |     '''Main Qubes application | ||||||
| @ -629,6 +670,27 @@ class Qubes(qubes.PropertyHolder): | |||||||
|     default_dispvm = qubes.VMProperty('default_dispvm', load_stage=3, |     default_dispvm = qubes.VMProperty('default_dispvm', load_stage=3, | ||||||
|         doc='Default DispVM base for service calls') |         doc='Default DispVM base for service calls') | ||||||
| 
 | 
 | ||||||
|  |     default_pool = qubes.property('default_pool', load_stage=3, | ||||||
|  |         default=_default_pool, | ||||||
|  |         doc='Default storage pool') | ||||||
|  | 
 | ||||||
|  |     default_pool_private = qubes.property('default_pool_private', load_stage=3, | ||||||
|  |         default=lambda app: app.default_pool, | ||||||
|  |         doc='Default storage pool for private volumes') | ||||||
|  | 
 | ||||||
|  |     default_pool_root = qubes.property('default_pool_root', load_stage=3, | ||||||
|  |         default=lambda app: app.default_pool, | ||||||
|  |         doc='Default storage pool for root volumes') | ||||||
|  | 
 | ||||||
|  |     default_pool_volatile = qubes.property('default_pool_volatile', | ||||||
|  |         load_stage=3, | ||||||
|  |         default=lambda app: app.default_pool, | ||||||
|  |         doc='Default storage pool for volatile volumes') | ||||||
|  | 
 | ||||||
|  |     default_pool_kernel = qubes.property('default_pool_kernel', load_stage=3, | ||||||
|  |         default=lambda app: app.default_pool, | ||||||
|  |         doc='Default storage pool for kernel volumes') | ||||||
|  | 
 | ||||||
|     # TODO #1637 #892 |     # TODO #1637 #892 | ||||||
|     check_updates_vm = qubes.property('check_updates_vm', |     check_updates_vm = qubes.property('check_updates_vm', | ||||||
|         type=bool, setter=qubes.property.bool, |         type=bool, setter=qubes.property.bool, | ||||||
| @ -662,7 +724,7 @@ class Qubes(qubes.PropertyHolder): | |||||||
|         else: |         else: | ||||||
|             self._store = os.environ.get('QUBES_XML_PATH', |             self._store = os.environ.get('QUBES_XML_PATH', | ||||||
|                 os.path.join( |                 os.path.join( | ||||||
|                     qubes.config.system_path['qubes_base_dir'], |                     qubes.config.qubes_base_dir, | ||||||
|                     qubes.config.system_path['qubes_store_filename'])) |                     qubes.config.system_path['qubes_store_filename'])) | ||||||
| 
 | 
 | ||||||
|         super(Qubes, self).__init__(xml=None, **kwargs) |         super(Qubes, self).__init__(xml=None, **kwargs) | ||||||
| @ -907,6 +969,8 @@ class Qubes(qubes.PropertyHolder): | |||||||
|         for name, config in qubes.config.defaults['pool_configs'].items(): |         for name, config in qubes.config.defaults['pool_configs'].items(): | ||||||
|             self.pools[name] = self._get_pool(**config) |             self.pools[name] = self._get_pool(**config) | ||||||
| 
 | 
 | ||||||
|  |         self.default_pool_kernel = 'linux-kernel' | ||||||
|  | 
 | ||||||
|         self.domains.add( |         self.domains.add( | ||||||
|             qubes.vm.adminvm.AdminVM(self, None, label='black')) |             qubes.vm.adminvm.AdminVM(self, None, label='black')) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -36,8 +36,6 @@ system_path = { | |||||||
|     'qrexec_client_path': '/usr/lib/qubes/qrexec-client', |     'qrexec_client_path': '/usr/lib/qubes/qrexec-client', | ||||||
|     'qubesdb_daemon_path': '/usr/sbin/qubesdb-daemon', |     'qubesdb_daemon_path': '/usr/sbin/qubesdb-daemon', | ||||||
| 
 | 
 | ||||||
|     'qubes_base_dir': qubes_base_dir, |  | ||||||
| 
 |  | ||||||
|     # Relative to qubes_base_dir |     # Relative to qubes_base_dir | ||||||
|     'qubes_appvms_dir': 'appvms', |     'qubes_appvms_dir': 'appvms', | ||||||
|     'qubes_templates_dir': 'vm-templates', |     'qubes_templates_dir': 'vm-templates', | ||||||
|  | |||||||
| @ -78,25 +78,26 @@ class Volume(object): | |||||||
|     domain = None |     domain = None | ||||||
|     path = None |     path = None | ||||||
|     script = None |     script = None | ||||||
|  |     #: disk space used by this volume, can be smaller than :py:attr:`size` | ||||||
|  |     #: for sparse volumes | ||||||
|     usage = 0 |     usage = 0 | ||||||
| 
 | 
 | ||||||
|     def __init__(self, name, pool, vid, internal=False, removable=False, |     def __init__(self, name, pool, vid, | ||||||
|             revisions_to_keep=0, rw=False, save_on_stop=False, size=0, |             revisions_to_keep=0, rw=False, save_on_stop=False, size=0, | ||||||
|             snap_on_start=False, source=None, **kwargs): |             snap_on_start=False, source=None, **kwargs): | ||||||
|         ''' Initialize a volume. |         ''' Initialize a volume. | ||||||
| 
 | 
 | ||||||
|             :param str name: The domain name |             :param str name: The name of the volume inside owning domain | ||||||
|             :param Pool pool: The pool object |             :param Pool pool: The pool object | ||||||
|             :param str vid:  Volume identifier needs to be unique in pool |             :param str vid:  Volume identifier needs to be unique in pool | ||||||
|             :param bool internal: If `True` volume is hidden when qvm-block ls |  | ||||||
|                 is used |  | ||||||
|             :param bool removable: If `True` volume can be detached from vm at |  | ||||||
|                 run time |  | ||||||
|             :param int revisions_to_keep: Amount of revisions to keep around |             :param int revisions_to_keep: Amount of revisions to keep around | ||||||
|             :param bool rw: If true volume will be mounted read-write |             :param bool rw: If true volume will be mounted read-write | ||||||
|             :param bool snap_on_start: Create a snapshot from source on start |             :param bool snap_on_start: Create a snapshot from source on | ||||||
|             :param bool save_on_stop: Write changes to disk in vm.stop() |                 start, instead of using volume own data | ||||||
|             :param Volume source: other volume in same pool, or None |             :param bool save_on_stop: Write changes to the volume in | ||||||
|  |                 vm.stop(), otherwise - discard | ||||||
|  |             :param Volume source: other volume in same pool to make snapshot | ||||||
|  |                 from, required if *snap_on_start*=`True` | ||||||
|             :param str/int size: Size of the volume |             :param str/int size: Size of the volume | ||||||
| 
 | 
 | ||||||
|         ''' |         ''' | ||||||
| @ -106,16 +107,34 @@ class Volume(object): | |||||||
|         assert source is None or (isinstance(source, Volume) |         assert source is None or (isinstance(source, Volume) | ||||||
|                                   and source.pool == pool) |                                   and source.pool == pool) | ||||||
| 
 | 
 | ||||||
|  |         if snap_on_start and source is None: | ||||||
|  |             msg = "snap_on_start specified on {!r} but no volume source set" | ||||||
|  |             msg = msg.format(name) | ||||||
|  |             raise StoragePoolException(msg) | ||||||
|  |         elif not snap_on_start and source is not None: | ||||||
|  |             msg = "source specified on {!r} but no snap_on_start set" | ||||||
|  |             msg = msg.format(name) | ||||||
|  |             raise StoragePoolException(msg) | ||||||
|  | 
 | ||||||
|  |         #: Name of the volume in a domain it's attached to (like `root` or | ||||||
|  |         #: `private`). | ||||||
|         self.name = str(name) |         self.name = str(name) | ||||||
|  |         #: :py:class:`Pool` instance owning this volume | ||||||
|         self.pool = pool |         self.pool = pool | ||||||
|         self.internal = internal |         #: How many revisions of the volume to keep. Each revision is created | ||||||
|         self.removable = removable |         #  at :py:meth:`stop`, if :py:attr:`save_on_stop` is True | ||||||
|         self.revisions_to_keep = int(revisions_to_keep) |         self.revisions_to_keep = int(revisions_to_keep) | ||||||
|  |         #: Should this volume be writable by domain. | ||||||
|         self.rw = rw |         self.rw = rw | ||||||
|  |         #: Should volume state be saved or discarded at :py:meth:`stop` | ||||||
|         self.save_on_stop = save_on_stop |         self.save_on_stop = save_on_stop | ||||||
|         self._size = int(size) |         self._size = int(size) | ||||||
|  |         #: Should the volume state be initialized with a snapshot of | ||||||
|  |         #: same-named volume of domain's template. | ||||||
|         self.snap_on_start = snap_on_start |         self.snap_on_start = snap_on_start | ||||||
|  |         #: source volume for :py:attr:`snap_on_start` volumes | ||||||
|         self.source = source |         self.source = source | ||||||
|  |         #: Volume unique (inside given pool) identifier | ||||||
|         self.vid = vid |         self.vid = vid | ||||||
| 
 | 
 | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
| @ -142,6 +161,10 @@ class Volume(object): | |||||||
|     def create(self): |     def create(self): | ||||||
|         ''' Create the given volume on disk. |         ''' Create the given volume on disk. | ||||||
| 
 | 
 | ||||||
|  |             This method is called only once in the volume lifetime. Before | ||||||
|  |             calling this method, no data on disk should be touched (in | ||||||
|  |             context of this volume). | ||||||
|  | 
 | ||||||
|             This can be implemented as a coroutine. |             This can be implemented as a coroutine. | ||||||
|         ''' |         ''' | ||||||
|         raise self._not_implemented("create") |         raise self._not_implemented("create") | ||||||
| @ -152,24 +175,38 @@ class Volume(object): | |||||||
|         This can be implemented as a coroutine.''' |         This can be implemented as a coroutine.''' | ||||||
|         raise self._not_implemented("remove") |         raise self._not_implemented("remove") | ||||||
| 
 | 
 | ||||||
|     def commit(self): |  | ||||||
|         ''' Write the snapshot to disk |  | ||||||
| 
 |  | ||||||
|         This can be implemented as a coroutine.''' |  | ||||||
|         raise self._not_implemented("commit") |  | ||||||
| 
 |  | ||||||
|     def export(self): |     def export(self): | ||||||
|         ''' Returns an object that can be `open()`. ''' |         ''' Returns a path to read the volume data from. | ||||||
|  | 
 | ||||||
|  |             Reading from this path when domain owning this volume is | ||||||
|  |             running (i.e. when :py:meth:`is_dirty` is True) should return the | ||||||
|  |             data from before domain startup. | ||||||
|  | 
 | ||||||
|  |             Reading from the path returned by this method should return the | ||||||
|  |             volume data. If extracting volume data require something more | ||||||
|  |             than just reading from file (for example connecting to some other | ||||||
|  |             domain, or decompressing the data), the returned path may be a pipe. | ||||||
|  |         ''' | ||||||
|         raise self._not_implemented("export") |         raise self._not_implemented("export") | ||||||
| 
 | 
 | ||||||
|     def import_data(self): |     def import_data(self): | ||||||
|         ''' Returns an object that can be `open()`. ''' |         ''' Returns a path to overwrite volume data. | ||||||
|  | 
 | ||||||
|  |             This method is called after volume was already :py:meth:`create`-ed. | ||||||
|  | 
 | ||||||
|  |             Writing to this path should overwrite volume data. If importing | ||||||
|  |             volume data require something more than just writing to a file ( | ||||||
|  |             for example connecting to some other domain, or converting data | ||||||
|  |             on the fly), the returned path may be a pipe. | ||||||
|  |         ''' | ||||||
|         raise self._not_implemented("import") |         raise self._not_implemented("import") | ||||||
| 
 | 
 | ||||||
|     def import_data_end(self, success): |     def import_data_end(self, success): | ||||||
|         ''' End data import operation. This may be used by pool |         ''' End the data import operation. This may be used by pool | ||||||
|         implementation to commit changes, cleanup temporary files etc. |         implementation to commit changes, cleanup temporary files etc. | ||||||
| 
 | 
 | ||||||
|  |         This method is called regardless the operation was successful or not. | ||||||
|  | 
 | ||||||
|         :param success: True if data import was successful, otherwise False |         :param success: True if data import was successful, otherwise False | ||||||
|         ''' |         ''' | ||||||
|         # by default do nothing |         # by default do nothing | ||||||
| @ -179,54 +216,60 @@ class Volume(object): | |||||||
|         ''' Imports data from a different volume (possibly in a different |         ''' Imports data from a different volume (possibly in a different | ||||||
|         pool. |         pool. | ||||||
| 
 | 
 | ||||||
|         The needs to be create()d first. |         The volume needs to be create()d first. | ||||||
| 
 | 
 | ||||||
|         This can be implemented as a coroutine. ''' |         This can be implemented as a coroutine. ''' | ||||||
|         # pylint: disable=unused-argument |         # pylint: disable=unused-argument | ||||||
|         raise self._not_implemented("import_volume") |         raise self._not_implemented("import_volume") | ||||||
| 
 | 
 | ||||||
|     def is_dirty(self): |     def is_dirty(self): | ||||||
|         ''' Return `True` if volume was not properly shutdown and commited ''' |         ''' Return `True` if volume was not properly shutdown and committed. | ||||||
|  | 
 | ||||||
|  |             This include the situation when domain owning the volume is still | ||||||
|  |             running. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|         raise self._not_implemented("is_dirty") |         raise self._not_implemented("is_dirty") | ||||||
| 
 | 
 | ||||||
|     def is_outdated(self): |     def is_outdated(self): | ||||||
|         ''' Returns `True` if the currently used `volume.source` of a snapshot |         ''' Returns `True` if this snapshot of a source volume (for | ||||||
|             volume is outdated. |         `snap_on_start`=True) is outdated. | ||||||
|         ''' |         ''' | ||||||
|         raise self._not_implemented("is_outdated") |         raise self._not_implemented("is_outdated") | ||||||
| 
 | 
 | ||||||
|     def recover(self): |  | ||||||
|         ''' Try to recover a :py:class:`Volume` or :py:class:`SnapVolume` ''' |  | ||||||
|         raise self._not_implemented("recover") |  | ||||||
| 
 |  | ||||||
|     def reset(self): |  | ||||||
|         ''' Drop and recreate volume without copying it's content from source. |  | ||||||
|         ''' |  | ||||||
|         raise self._not_implemented("reset") |  | ||||||
| 
 |  | ||||||
|     def resize(self, size): |     def resize(self, size): | ||||||
|         ''' Expands volume, throws |         ''' Expands volume, throws | ||||||
|             :py:class:`qubes.storage.StoragePoolException` if |             :py:class:`qubes.storage.StoragePoolException` if | ||||||
|             given size is less than current_size |             given size is less than current_size | ||||||
| 
 | 
 | ||||||
|             This can be implemented as a coroutine. |             This can be implemented as a coroutine. | ||||||
|  | 
 | ||||||
|  |             :param int size: new size in bytes | ||||||
|         ''' |         ''' | ||||||
|         # pylint: disable=unused-argument |         # pylint: disable=unused-argument | ||||||
|         raise self._not_implemented("resize") |         raise self._not_implemented("resize") | ||||||
| 
 | 
 | ||||||
|     def revert(self, revision=None): |     def revert(self, revision=None): | ||||||
|         ''' Revert volume to previous revision  ''' |         ''' Revert volume to previous revision | ||||||
|  | 
 | ||||||
|  |         :param revision: revision to revert volume to, see :py:attr:`revisions` | ||||||
|  |         ''' | ||||||
|         # pylint: disable=unused-argument |         # pylint: disable=unused-argument | ||||||
|         raise self._not_implemented("revert") |         raise self._not_implemented("revert") | ||||||
| 
 | 
 | ||||||
|     def start(self): |     def start(self): | ||||||
|         ''' Do what ever is needed on start |         ''' Do what ever is needed on start. | ||||||
|  | 
 | ||||||
|  |         This include making a snapshot of template's volume if | ||||||
|  |         :py:attr:`snap_on_start` is set. | ||||||
| 
 | 
 | ||||||
|         This can be implemented as a coroutine.''' |         This can be implemented as a coroutine.''' | ||||||
|         raise self._not_implemented("start") |         raise self._not_implemented("start") | ||||||
| 
 | 
 | ||||||
|     def stop(self): |     def stop(self): | ||||||
|         ''' Do what ever is needed on stop |         ''' Do what ever is needed on stop. | ||||||
|  | 
 | ||||||
|  |         This include committing data if :py:attr:`save_on_stop` is set. | ||||||
| 
 | 
 | ||||||
|         This can be implemented as a coroutine.''' |         This can be implemented as a coroutine.''' | ||||||
| 
 | 
 | ||||||
| @ -245,12 +288,14 @@ class Volume(object): | |||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def revisions(self): |     def revisions(self): | ||||||
|         ''' Returns a `dict` containing revision identifiers and paths ''' |         ''' Returns a dict containing revision identifiers and time of their | ||||||
|  |         creation ''' | ||||||
|         msg = "{!s} has revisions not implemented".format(self.__class__) |         msg = "{!s} has revisions not implemented".format(self.__class__) | ||||||
|         raise NotImplementedError(msg) |         raise NotImplementedError(msg) | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def size(self): |     def size(self): | ||||||
|  |         ''' Volume size in bytes ''' | ||||||
|         return self._size |         return self._size | ||||||
| 
 | 
 | ||||||
|     @size.setter |     @size.setter | ||||||
| @ -262,29 +307,19 @@ class Volume(object): | |||||||
|     @property |     @property | ||||||
|     def config(self): |     def config(self): | ||||||
|         ''' return config data for serialization to qubes.xml ''' |         ''' return config data for serialization to qubes.xml ''' | ||||||
|         result = {'name': self.name, 'pool': str(self.pool), 'vid': self.vid, } |         result = { | ||||||
| 
 |             'name': self.name, | ||||||
|         if self.internal: |             'pool': str(self.pool), | ||||||
|             result['internal'] = self.internal |             'vid': self.vid, | ||||||
| 
 |             'revisions_to_keep': self.revisions_to_keep, | ||||||
|         if self.removable: |             'rw': self.rw, | ||||||
|             result['removable'] = self.removable |             'save_on_stop': self.save_on_stop, | ||||||
| 
 |             'snap_on_start': self.snap_on_start, | ||||||
|         if self.revisions_to_keep: |         } | ||||||
|             result['revisions_to_keep'] = self.revisions_to_keep |  | ||||||
| 
 |  | ||||||
|         if self.rw: |  | ||||||
|             result['rw'] = self.rw |  | ||||||
| 
 |  | ||||||
|         if self.save_on_stop: |  | ||||||
|             result['save_on_stop'] = self.save_on_stop |  | ||||||
| 
 | 
 | ||||||
|         if self.size: |         if self.size: | ||||||
|             result['size'] = self.size |             result['size'] = self.size | ||||||
| 
 | 
 | ||||||
|         if self.snap_on_start: |  | ||||||
|             result['snap_on_start'] = self.snap_on_start |  | ||||||
| 
 |  | ||||||
|         if self.source: |         if self.source: | ||||||
|             result['source'] = str(self.source) |             result['source'] = str(self.source) | ||||||
| 
 | 
 | ||||||
| @ -335,7 +370,13 @@ class Storage(object): | |||||||
| 
 | 
 | ||||||
|         if 'name' not in volume_config: |         if 'name' not in volume_config: | ||||||
|             volume_config['name'] = name |             volume_config['name'] = name | ||||||
|  |         if 'pool' not in volume_config: | ||||||
|  |             pool = getattr(self.vm.app, 'default_pool_' + name) | ||||||
|  |         else: | ||||||
|             pool = self.vm.app.get_pool(volume_config['pool']) |             pool = self.vm.app.get_pool(volume_config['pool']) | ||||||
|  |         if 'internal' in volume_config: | ||||||
|  |             # migrate old config | ||||||
|  |             del volume_config['internal'] | ||||||
|         volume = pool.init_volume(self.vm, volume_config) |         volume = pool.init_volume(self.vm, volume_config) | ||||||
|         self.vm.volumes[name] = volume |         self.vm.volumes[name] = volume | ||||||
|         return volume |         return volume | ||||||
| @ -486,13 +527,6 @@ class Storage(object): | |||||||
| 
 | 
 | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|     def rename(self, old_name, new_name): |  | ||||||
|         ''' Notify the pools that the domain was renamed ''' |  | ||||||
|         volumes = self.vm.volumes |  | ||||||
|         for name, volume in volumes.items(): |  | ||||||
|             pool = volume.pool |  | ||||||
|             volumes[name] = pool.rename(volume, old_name, new_name) |  | ||||||
| 
 |  | ||||||
|     @asyncio.coroutine |     @asyncio.coroutine | ||||||
|     def verify(self): |     def verify(self): | ||||||
|         '''Verify that the storage is sane. |         '''Verify that the storage is sane. | ||||||
| @ -559,19 +593,6 @@ class Storage(object): | |||||||
|         if futures: |         if futures: | ||||||
|             yield from asyncio.wait(futures) |             yield from asyncio.wait(futures) | ||||||
| 
 | 
 | ||||||
|     @asyncio.coroutine |  | ||||||
|     def commit(self): |  | ||||||
|         ''' Makes changes to an 'origin' volume persistent ''' |  | ||||||
|         futures = [] |  | ||||||
|         for volume in self.vm.volumes.values(): |  | ||||||
|             if volume.save_on_stop: |  | ||||||
|                 ret = volume.commit() |  | ||||||
|                 if asyncio.iscoroutine(ret): |  | ||||||
|                     futures.append(ret) |  | ||||||
| 
 |  | ||||||
|         if futures: |  | ||||||
|             yield asyncio.wait(futures) |  | ||||||
| 
 |  | ||||||
|     def unused_frontend(self): |     def unused_frontend(self): | ||||||
|         ''' Find an unused device name ''' |         ''' Find an unused device name ''' | ||||||
|         unused_frontends = self.AVAILABLE_FRONTENDS.difference( |         unused_frontends = self.AVAILABLE_FRONTENDS.difference( | ||||||
| @ -722,10 +743,6 @@ class Pool(object): | |||||||
|         ''' |         ''' | ||||||
|         raise self._not_implemented("init_volume") |         raise self._not_implemented("init_volume") | ||||||
| 
 | 
 | ||||||
|     def rename(self, volume, old_name, new_name): |  | ||||||
|         ''' Called when the domain changes its name ''' |  | ||||||
|         raise self._not_implemented("rename") |  | ||||||
| 
 |  | ||||||
|     def setup(self): |     def setup(self): | ||||||
|         ''' Called when adding a pool to the system. Use this for implementation |         ''' Called when adding a pool to the system. Use this for implementation | ||||||
|             specific set up. |             specific set up. | ||||||
|  | |||||||
| @ -37,6 +37,20 @@ BLKSIZE = 512 | |||||||
| 
 | 
 | ||||||
| class FilePool(qubes.storage.Pool): | class FilePool(qubes.storage.Pool): | ||||||
|     ''' File based 'original' disk implementation |     ''' File based 'original' disk implementation | ||||||
|  | 
 | ||||||
|  |     Volumes are stored in sparse files. Additionally device-mapper is used for | ||||||
|  |     applying copy-on-write layer. | ||||||
|  | 
 | ||||||
|  |     Quick reference on device-mapper layers: | ||||||
|  | 
 | ||||||
|  |     snap_on_start save_on_stop layout | ||||||
|  |     yes           yes          not supported | ||||||
|  |     no            yes          snapshot-origin(volume.img, volume-cow.img) | ||||||
|  |     yes           no           snapshot( | ||||||
|  |                                    snapshot(source.img, source-cow.img), | ||||||
|  |                                    volume-cow.img) | ||||||
|  |     no            no           volume.img directly | ||||||
|  | 
 | ||||||
|     '''  # pylint: disable=protected-access |     '''  # pylint: disable=protected-access | ||||||
|     driver = 'file' |     driver = 'file' | ||||||
| 
 | 
 | ||||||
| @ -57,16 +71,18 @@ class FilePool(qubes.storage.Pool): | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     def init_volume(self, vm, volume_config): |     def init_volume(self, vm, volume_config): | ||||||
|  |         if volume_config.get('snap_on_start', False) and \ | ||||||
|  |                 volume_config.get('save_on_stop', False): | ||||||
|  |             raise NotImplementedError( | ||||||
|  |                 'snap_on_start + save_on_stop not supported by file driver') | ||||||
|         volume_config['dir_path'] = self.dir_path |         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: |         if 'vid' not in volume_config: | ||||||
|             volume_config['vid'] = os.path.join( |             volume_config['vid'] = os.path.join( | ||||||
|                 self._vid_prefix(vm), volume_config['name']) |                 self._vid_prefix(vm), volume_config['name']) | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             if volume_config['reset_on_start']: |             if not volume_config.get('save_on_stop', False): | ||||||
|                 volume_config['revisions_to_keep'] = 0 |                 volume_config['revisions_to_keep'] = 0 | ||||||
|         except KeyError: |         except KeyError: | ||||||
|             pass |             pass | ||||||
| @ -74,31 +90,15 @@ class FilePool(qubes.storage.Pool): | |||||||
|             if 'revisions_to_keep' not in volume_config: |             if 'revisions_to_keep' not in volume_config: | ||||||
|                 volume_config['revisions_to_keep'] = self.revisions_to_keep |                 volume_config['revisions_to_keep'] = self.revisions_to_keep | ||||||
| 
 | 
 | ||||||
|  |         if int(volume_config['revisions_to_keep']) > 1: | ||||||
|  |             raise NotImplementedError( | ||||||
|  |                 'FilePool supports maximum 1 volume revision to keep') | ||||||
|  | 
 | ||||||
|         volume_config['pool'] = self |         volume_config['pool'] = self | ||||||
|         volume = FileVolume(**volume_config) |         volume = FileVolume(**volume_config) | ||||||
|         self._volumes += [volume] |         self._volumes += [volume] | ||||||
|         return volume |         return volume | ||||||
| 
 | 
 | ||||||
|     def rename(self, volume, old_name, new_name): |  | ||||||
|         assert issubclass(volume.__class__, FileVolume) |  | ||||||
|         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) |  | ||||||
|             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) |  | ||||||
| 
 |  | ||||||
|         return volume |  | ||||||
| 
 |  | ||||||
|     def destroy(self): |     def destroy(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| @ -150,59 +150,37 @@ class FileVolume(qubes.storage.Volume): | |||||||
|     ''' Parent class for the xen volumes implementation which expects a |     ''' Parent class for the xen volumes implementation which expects a | ||||||
|         `target_dir` param on initialization.  ''' |         `target_dir` param on initialization.  ''' | ||||||
| 
 | 
 | ||||||
|     def __init__(self, dir_path, backward_comp=False, **kwargs): |     def __init__(self, dir_path, **kwargs): | ||||||
|         self.dir_path = dir_path |         self.dir_path = dir_path | ||||||
|         self.backward_comp = backward_comp |  | ||||||
|         assert self.dir_path, "dir_path not specified" |         assert self.dir_path, "dir_path not specified" | ||||||
|         super(FileVolume, self).__init__(**kwargs) |         super(FileVolume, self).__init__(**kwargs) | ||||||
| 
 | 
 | ||||||
|         if self.snap_on_start and self.source is None: |         if self.snap_on_start: | ||||||
|             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: |  | ||||||
|             img_name = self.source.vid + '-cow.img' |             img_name = self.source.vid + '-cow.img' | ||||||
|             self.path_source_cow = os.path.join(self.dir_path, img_name) |             self.path_source_cow = os.path.join(self.dir_path, img_name) | ||||||
|         elif self._is_volume or self._is_volatile: |  | ||||||
|             pass |  | ||||||
|         elif self._is_origin: |  | ||||||
|             pass |  | ||||||
|         else: |  | ||||||
|             assert False, 'This should not happen' |  | ||||||
| 
 | 
 | ||||||
|     def create(self): |     def create(self): | ||||||
|         assert isinstance(self.size, int) and self.size > 0, \ |         assert isinstance(self.size, int) and self.size > 0, \ | ||||||
|             'Volatile volume size must be > 0' |             'Volume size must be > 0' | ||||||
|         if self._is_origin: |         if not self.snap_on_start: | ||||||
|             create_sparse_file(self.path, self.size) |             create_sparse_file(self.path, self.size) | ||||||
|  |         # path_cow not needed only in volatile volume | ||||||
|  |         if self.save_on_stop or self.snap_on_start: | ||||||
|             create_sparse_file(self.path_cow, self.size) |             create_sparse_file(self.path_cow, self.size) | ||||||
|         elif not self._is_snapshot: |  | ||||||
|             if self.source is not None: |  | ||||||
|                 source_path = os.path.join(self.dir_path, |  | ||||||
|                     self.source.vid + '.img') |  | ||||||
|                 copy_file(source_path, self.path) |  | ||||||
|             elif self._is_volatile: |  | ||||||
|                 pass |  | ||||||
|             else: |  | ||||||
|                 create_sparse_file(self.path, self.size) |  | ||||||
| 
 | 
 | ||||||
|     def remove(self): |     def remove(self): | ||||||
|         if not self.internal: |         if not self.snap_on_start: | ||||||
|             return  # do not remove random attached file volumes |  | ||||||
|         elif self._is_snapshot: |  | ||||||
|             return  # no need to remove, because it's just a snapshot |  | ||||||
|         else: |  | ||||||
|             _remove_if_exists(self.path) |             _remove_if_exists(self.path) | ||||||
|             if self._is_origin: |         if self.snap_on_start or self.save_on_stop: | ||||||
|             _remove_if_exists(self.path_cow) |             _remove_if_exists(self.path_cow) | ||||||
| 
 | 
 | ||||||
|     def is_dirty(self): |     def is_dirty(self): | ||||||
|         return False  # TODO: How to implement this? |         if not self.save_on_stop: | ||||||
|  |             return False | ||||||
|  |         if os.path.exists(self.path_cow): | ||||||
|  |             stat = os.stat(self.path_cow) | ||||||
|  |             return stat.st_blocks > 0 | ||||||
|  |         return False | ||||||
| 
 | 
 | ||||||
|     def resize(self, size): |     def resize(self, size): | ||||||
|         ''' Expands volume, throws |         ''' Expands volume, throws | ||||||
| @ -223,7 +201,7 @@ class FileVolume(qubes.storage.Volume): | |||||||
|         with open(self.path, 'a+b') as fd: |         with open(self.path, 'a+b') as fd: | ||||||
|             fd.truncate(size) |             fd.truncate(size) | ||||||
| 
 | 
 | ||||||
|         p = subprocess.Popen(['sudo', 'losetup', '--associated', self.path], |         p = subprocess.Popen(['losetup', '--associated', self.path], | ||||||
|                              stdout=subprocess.PIPE) |                              stdout=subprocess.PIPE) | ||||||
|         result = p.communicate() |         result = p.communicate() | ||||||
| 
 | 
 | ||||||
| @ -232,28 +210,27 @@ class FileVolume(qubes.storage.Volume): | |||||||
|             loop_dev = m.group(1) |             loop_dev = m.group(1) | ||||||
| 
 | 
 | ||||||
|             # resize loop device |             # resize loop device | ||||||
|             subprocess.check_call(['sudo', 'losetup', '--set-capacity', |             subprocess.check_call(['losetup', '--set-capacity', | ||||||
|                                    loop_dev]) |                                    loop_dev]) | ||||||
|         self.size = size |         self.size = size | ||||||
| 
 | 
 | ||||||
|     def commit(self): |     def commit(self): | ||||||
|         msg = 'Tried to commit a non commitable volume {!r}'.format(self) |         msg = 'Tried to commit a non commitable volume {!r}'.format(self) | ||||||
|         assert (self._is_origin or self._is_volume) and self.rw, msg |         assert self.save_on_stop and self.rw, msg | ||||||
| 
 |  | ||||||
|         if self._is_volume: |  | ||||||
|             return self |  | ||||||
| 
 | 
 | ||||||
|         if os.path.exists(self.path_cow): |         if os.path.exists(self.path_cow): | ||||||
|  |             if self.revisions_to_keep: | ||||||
|                 old_path = self.path_cow + '.old' |                 old_path = self.path_cow + '.old' | ||||||
|                 os.rename(self.path_cow, old_path) |                 os.rename(self.path_cow, old_path) | ||||||
|  |             else: | ||||||
|  |                 os.unlink(self.path_cow) | ||||||
| 
 | 
 | ||||||
|         old_umask = os.umask(0o002) |         create_sparse_file(self.path_cow, self.size) | ||||||
|         with open(self.path_cow, 'w') as f_cow: |  | ||||||
|             f_cow.truncate(self.size) |  | ||||||
|         os.umask(old_umask) |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def export(self): |     def export(self): | ||||||
|  |         # FIXME: this should rather return snapshot(self.path, self.path_cow) | ||||||
|  |         #  if domain is running | ||||||
|         return self.path |         return self.path | ||||||
| 
 | 
 | ||||||
|     def import_volume(self, src_volume): |     def import_volume(self, src_volume): | ||||||
| @ -271,53 +248,30 @@ class FileVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|     def reset(self): |     def reset(self): | ||||||
|         ''' Remove and recreate a volatile volume ''' |         ''' Remove and recreate a volatile volume ''' | ||||||
|         assert self._is_volatile, "Not a volatile volume" |         assert not self.snap_on_start and not self.save_on_stop, \ | ||||||
|  |             "Not a volatile volume" | ||||||
|         assert isinstance(self.size, int) and self.size > 0, \ |         assert isinstance(self.size, int) and self.size > 0, \ | ||||||
|             'Volatile volume size must be > 0' |             'Volatile volume size must be > 0' | ||||||
| 
 | 
 | ||||||
|         _remove_if_exists(self.path) |         _remove_if_exists(self.path) | ||||||
| 
 |         create_sparse_file(self.path, self.size) | ||||||
|         with open(self.path, "w") as f_volatile: |  | ||||||
|             f_volatile.truncate(self.size) |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def revert(self, revision=None): |  | ||||||
|         if revision is not None: |  | ||||||
|             try: |  | ||||||
|                 return self.revisions[revision] |  | ||||||
|             except KeyError: |  | ||||||
|                 msg = "Volume {!r} does not have revision {!s}" |  | ||||||
|                 msg = msg.format(self, revision) |  | ||||||
|                 raise qubes.storage.StoragePoolException(msg) |  | ||||||
|         else: |  | ||||||
|             try: |  | ||||||
|                 old_path = self.revisions.values().pop() |  | ||||||
|                 os.rename(old_path, self.path_cow) |  | ||||||
|             except IndexError: |  | ||||||
|                 msg = "Volume {!r} does not have old revisions".format(self) |  | ||||||
|                 raise qubes.storage.StoragePoolException(msg) |  | ||||||
| 
 |  | ||||||
|     def start(self): |     def start(self): | ||||||
|         if self._is_volatile: |         if not self.save_on_stop and not self.snap_on_start: | ||||||
|             self.reset() |             self.reset() | ||||||
|         else: |         else: | ||||||
|             _check_path(self.path) |  | ||||||
|             if self.snap_on_start: |  | ||||||
|             if not self.save_on_stop: |             if not self.save_on_stop: | ||||||
|                 # make sure previous snapshot is removed - even if VM |                 # make sure previous snapshot is removed - even if VM | ||||||
|                     # shutdown routing wasn't called (power interrupt or so) |                 # shutdown routine wasn't called (power interrupt or so) | ||||||
|                 _remove_if_exists(self.path_cow) |                 _remove_if_exists(self.path_cow) | ||||||
|                 try: |             if not os.path.exists(self.path_cow): | ||||||
|                     _check_path(self.path_cow) |  | ||||||
|                 except qubes.storage.StoragePoolException: |  | ||||||
|                 create_sparse_file(self.path_cow, self.size) |                 create_sparse_file(self.path_cow, self.size) | ||||||
|                     _check_path(self.path_cow) |             if not self.snap_on_start: | ||||||
|  |                 _check_path(self.path) | ||||||
|             if hasattr(self, 'path_source_cow'): |             if hasattr(self, 'path_source_cow'): | ||||||
|                     try: |                 if not os.path.exists(self.path_source_cow): | ||||||
|                         _check_path(self.path_source_cow) |  | ||||||
|                     except qubes.storage.StoragePoolException: |  | ||||||
|                     create_sparse_file(self.path_source_cow, self.size) |                     create_sparse_file(self.path_source_cow, self.size) | ||||||
|                         _check_path(self.path_source_cow) |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def stop(self): |     def stop(self): | ||||||
| @ -331,7 +285,7 @@ class FileVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def path(self): |     def path(self): | ||||||
|         if self._is_snapshot: |         if self.snap_on_start: | ||||||
|             return os.path.join(self.dir_path, self.source.vid + '.img') |             return os.path.join(self.dir_path, self.source.vid + '.img') | ||||||
|         return os.path.join(self.dir_path, self.vid + '.img') |         return os.path.join(self.dir_path, self.vid + '.img') | ||||||
| 
 | 
 | ||||||
| @ -342,18 +296,19 @@ class FileVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|     def verify(self): |     def verify(self): | ||||||
|         ''' Verifies the volume. ''' |         ''' Verifies the volume. ''' | ||||||
|         if not os.path.exists(self.path) and not self._is_volatile: |         if not os.path.exists(self.path) and \ | ||||||
|  |                 (self.snap_on_start or self.save_on_stop): | ||||||
|             msg = 'Missing image file: {!s}.'.format(self.path) |             msg = 'Missing image file: {!s}.'.format(self.path) | ||||||
|             raise qubes.storage.StoragePoolException(msg) |             raise qubes.storage.StoragePoolException(msg) | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def script(self): |     def script(self): | ||||||
|         if self._is_volume or self._is_volatile: |         if not self.snap_on_start and not self.save_on_stop: | ||||||
|             return None |             return None | ||||||
|         elif self._is_origin: |         elif not self.snap_on_start and self.save_on_stop: | ||||||
|             return 'block-origin' |             return 'block-origin' | ||||||
|         elif self._is_origin_snapshot or self._is_snapshot: |         elif self.snap_on_start: | ||||||
|             return 'block-snapshot' |             return 'block-snapshot' | ||||||
| 
 | 
 | ||||||
|     def block_device(self): |     def block_device(self): | ||||||
| @ -361,9 +316,9 @@ class FileVolume(qubes.storage.Volume): | |||||||
|             the libvirt XML template as <disk>. |             the libvirt XML template as <disk>. | ||||||
|         ''' |         ''' | ||||||
|         path = self.path |         path = self.path | ||||||
|         if self._is_snapshot: |         if self.snap_on_start: | ||||||
|             path += ":" + self.path_source_cow |             path += ":" + self.path_source_cow | ||||||
|         if self._is_origin or self._is_snapshot: |         if self.snap_on_start or self.save_on_stop: | ||||||
|             path += ":" + self.path_cow |             path += ":" + self.path_cow | ||||||
|         return qubes.storage.BlockDevice(path, self.name, self.script, self.rw, |         return qubes.storage.BlockDevice(path, self.name, self.script, self.rw, | ||||||
|                                          self.domain, self.devtype) |                                          self.domain, self.devtype) | ||||||
| @ -380,39 +335,13 @@ class FileVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|         seconds = os.path.getctime(old_revision) |         seconds = os.path.getctime(old_revision) | ||||||
|         iso_date = qubes.storage.isodate(seconds).split('.', 1)[0] |         iso_date = qubes.storage.isodate(seconds).split('.', 1)[0] | ||||||
|         return {iso_date: old_revision} |         return {'old': iso_date} | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def usage(self): |     def usage(self): | ||||||
|         ''' Returns the actualy used space ''' |         ''' Returns the actualy used space ''' | ||||||
|         return get_disk_usage(self.vid) |         return get_disk_usage(self.vid) | ||||||
| 
 | 
 | ||||||
|     @property |  | ||||||
|     def _is_volatile(self): |  | ||||||
|         ''' Internal helper. Useful for differentiating volume handling ''' |  | ||||||
|         return not self.snap_on_start and not self.save_on_stop |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     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 |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def _is_snapshot(self): |  | ||||||
|         ''' Internal helper. Useful for differentiating volume handling ''' |  | ||||||
|         return self.snap_on_start and not self.save_on_stop |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def _is_origin_snapshot(self): |  | ||||||
|         ''' Internal helper. Useful for differentiating volume handling ''' |  | ||||||
|         return self.snap_on_start and self.save_on_stop |  | ||||||
| 
 |  | ||||||
|     @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 |  | ||||||
| 
 | 
 | ||||||
| def create_sparse_file(path, size): | def create_sparse_file(path, size): | ||||||
|     ''' Create an empty sparse file ''' |     ''' Create an empty sparse file ''' | ||||||
|  | |||||||
| @ -138,6 +138,14 @@ class LinuxKernel(Pool): | |||||||
|     def init_volume(self, vm, volume_config): |     def init_volume(self, vm, volume_config): | ||||||
|         assert not volume_config['rw'] |         assert not volume_config['rw'] | ||||||
| 
 | 
 | ||||||
|  |         # migrate old config | ||||||
|  |         if volume_config.get('snap_on_start', False) and not \ | ||||||
|  |                 volume_config.get('source', None): | ||||||
|  |             volume_config['snap_on_start'] = False | ||||||
|  | 
 | ||||||
|  |         if volume_config.get('save_on_stop', False): | ||||||
|  |             raise NotImplementedError( | ||||||
|  |                 'LinuxKernel pool does not support save_on_stop=True') | ||||||
|         volume_config['pool'] = self |         volume_config['pool'] = self | ||||||
|         volume = LinuxModules(self.dir_path, lambda: vm.kernel, **volume_config) |         volume = LinuxModules(self.dir_path, lambda: vm.kernel, **volume_config) | ||||||
| 
 | 
 | ||||||
| @ -157,9 +165,6 @@ class LinuxKernel(Pool): | |||||||
|     def import_volume(self, dst_pool, dst_volume, src_pool, src_volume): |     def import_volume(self, dst_pool, dst_volume, src_pool, src_volume): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|     def rename(self, volume, old_name, new_name): |  | ||||||
|         return volume |  | ||||||
| 
 |  | ||||||
|     def setup(self): |     def setup(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| @ -170,7 +175,6 @@ class LinuxKernel(Pool): | |||||||
|                              kernel_version, |                              kernel_version, | ||||||
|                              pool=self, |                              pool=self, | ||||||
|                              name=kernel_version, |                              name=kernel_version, | ||||||
|                              internal=True, |  | ||||||
|                              rw=False |                              rw=False | ||||||
|                              ) |                              ) | ||||||
|                 for kernel_version in os.listdir(self.dir_path)] |                 for kernel_version in os.listdir(self.dir_path)] | ||||||
|  | |||||||
| @ -21,9 +21,14 @@ | |||||||
| ''' Driver for storing vm images in a LVM thin pool ''' | ''' Driver for storing vm images in a LVM thin pool ''' | ||||||
| 
 | 
 | ||||||
| import logging | import logging | ||||||
|  | import operator | ||||||
| import os | import os | ||||||
| import subprocess | import subprocess | ||||||
| 
 | 
 | ||||||
|  | import time | ||||||
|  | 
 | ||||||
|  | import asyncio | ||||||
|  | 
 | ||||||
| import qubes | import qubes | ||||||
| import qubes.storage | import qubes.storage | ||||||
| import qubes.utils | import qubes.utils | ||||||
| @ -88,23 +93,6 @@ class ThinPool(qubes.storage.Pool): | |||||||
|         volume_config['pool'] = self |         volume_config['pool'] = self | ||||||
|         return ThinVolume(**volume_config) |         return ThinVolume(**volume_config) | ||||||
| 
 | 
 | ||||||
|     def rename(self, volume, old_name, new_name): |  | ||||||
|         ''' Called when the domain changes its name ''' |  | ||||||
|         new_vid = "{!s}/vm-{!s}-{!s}".format(self.volume_group, new_name, |  | ||||||
|                                           volume.name) |  | ||||||
|         if volume.save_on_stop: |  | ||||||
|             cmd = ['clone', volume.vid, new_vid] |  | ||||||
|             qubes_lvm(cmd, self.log) |  | ||||||
|             cmd = ['remove', volume.vid] |  | ||||||
|             qubes_lvm(cmd, self.log) |  | ||||||
| 
 |  | ||||||
|         volume.vid = new_vid |  | ||||||
| 
 |  | ||||||
|         if volume.snap_on_start: |  | ||||||
|             volume._vid_snap = volume.vid + '-snap' |  | ||||||
|         reset_cache() |  | ||||||
|         return volume |  | ||||||
| 
 |  | ||||||
|     def setup(self): |     def setup(self): | ||||||
|         pass  # TODO Should we create a non existing pool? |         pass  # TODO Should we create a non existing pool? | ||||||
| 
 | 
 | ||||||
| @ -119,6 +107,9 @@ class ThinPool(qubes.storage.Pool): | |||||||
|             if vid.endswith('-snap'): |             if vid.endswith('-snap'): | ||||||
|                 # implementation detail volume |                 # implementation detail volume | ||||||
|                 continue |                 continue | ||||||
|  |             if vid.endswith('-back'): | ||||||
|  |                 # old revisions | ||||||
|  |                 continue | ||||||
|             config = { |             config = { | ||||||
|                 'pool': self, |                 'pool': self, | ||||||
|                 'vid': vid, |                 'vid': vid, | ||||||
| @ -132,7 +123,7 @@ class ThinPool(qubes.storage.Pool): | |||||||
| 
 | 
 | ||||||
| def init_cache(log=logging.getLogger('qube.storage.lvm')): | def init_cache(log=logging.getLogger('qube.storage.lvm')): | ||||||
|     cmd = ['lvs', '--noheadings', '-o', |     cmd = ['lvs', '--noheadings', '-o', | ||||||
|            'vg_name,pool_lv,name,lv_size,data_percent,lv_attr', |            'vg_name,pool_lv,name,lv_size,data_percent,lv_attr,origin', | ||||||
|            '--units', 'b', '--separator', ','] |            '--units', 'b', '--separator', ','] | ||||||
|     if os.getuid() != 0: |     if os.getuid() != 0: | ||||||
|         cmd.insert(0, 'sudo') |         cmd.insert(0, 'sudo') | ||||||
| @ -149,14 +140,15 @@ def init_cache(log=logging.getLogger('qube.storage.lvm')): | |||||||
| 
 | 
 | ||||||
|     for line in out.splitlines(): |     for line in out.splitlines(): | ||||||
|         line = line.decode().strip() |         line = line.decode().strip() | ||||||
|         pool_name, pool_lv, name, size, usage_percent, attr = line.split(',', 5) |         pool_name, pool_lv, name, size, usage_percent, attr, \ | ||||||
|  |             origin = line.split(',', 6) | ||||||
|         if '' in [pool_name, pool_lv, name, size, usage_percent]: |         if '' in [pool_name, pool_lv, name, size, usage_percent]: | ||||||
|             continue |             continue | ||||||
|         name = pool_name + "/" + name |         name = pool_name + "/" + name | ||||||
|         size = int(size[:-1]) |         size = int(size[:-1]) | ||||||
|         usage = int(size / 100 * float(usage_percent)) |         usage = int(size / 100 * float(usage_percent)) | ||||||
|         result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv, |         result[name] = {'size': size, 'usage': usage, 'pool_lv': pool_lv, | ||||||
|             'attr': attr} |             'attr': attr, 'origin': origin} | ||||||
| 
 | 
 | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| @ -173,16 +165,7 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|         super(ThinVolume, self).__init__(size=size, **kwargs) |         super(ThinVolume, self).__init__(size=size, **kwargs) | ||||||
|         self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool)) |         self.log = logging.getLogger('qube.storage.lvm.%s' % str(self.pool)) | ||||||
| 
 | 
 | ||||||
|         if self.snap_on_start and self.source is None: |         if self.snap_on_start or self.save_on_stop: | ||||||
|             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.snap_on_start: |  | ||||||
|             self._vid_snap = self.vid + '-snap' |             self._vid_snap = self.vid + '-snap' | ||||||
| 
 | 
 | ||||||
|         self._size = size |         self._size = size | ||||||
| @ -193,28 +176,18 @@ class ThinVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def revisions(self): |     def revisions(self): | ||||||
|         path = self.path + '-back' |         name_prefix = self.vid + '-' | ||||||
|         if os.path.exists(path): |         revisions = {} | ||||||
|             seconds = os.path.getctime(path) |         for revision_vid in size_cache: | ||||||
|  |             if not revision_vid.startswith(name_prefix): | ||||||
|  |                 continue | ||||||
|  |             if not revision_vid.endswith('-back'): | ||||||
|  |                 continue | ||||||
|  |             revision_vid = revision_vid[len(name_prefix):] | ||||||
|  |             seconds = int(revision_vid[:-len('-back')]) | ||||||
|             iso_date = qubes.storage.isodate(seconds).split('.', 1)[0] |             iso_date = qubes.storage.isodate(seconds).split('.', 1)[0] | ||||||
|             return {iso_date: path} |             revisions[revision_vid] = iso_date | ||||||
|         return {} |         return revisions | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def _is_origin(self): |  | ||||||
|         return not self.snap_on_start and self.save_on_stop |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def _is_origin_snapshot(self): |  | ||||||
|         return self.snap_on_start and self.save_on_stop |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def _is_snapshot(self): |  | ||||||
|         return self.snap_on_start and not self.save_on_stop |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def _is_volatile(self): |  | ||||||
|         return not self.snap_on_start and not self.save_on_stop |  | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def size(self): |     def size(self): | ||||||
| @ -230,8 +203,8 @@ class ThinVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|     def _reset(self): |     def _reset(self): | ||||||
|         ''' Resets a volatile volume ''' |         ''' Resets a volatile volume ''' | ||||||
|         assert self._is_volatile, \ |         assert not self.snap_on_start and not self.save_on_stop, \ | ||||||
|             'Expected a volatile volume, but got {!r}'.format(self) |             "Not a volatile volume" | ||||||
|         self.log.debug('Resetting volatile ' + self.vid) |         self.log.debug('Resetting volatile ' + self.vid) | ||||||
|         try: |         try: | ||||||
|             cmd = ['remove', self.vid] |             cmd = ['remove', self.vid] | ||||||
| @ -243,6 +216,27 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|                str(self.size)] |                str(self.size)] | ||||||
|         qubes_lvm(cmd, self.log) |         qubes_lvm(cmd, self.log) | ||||||
| 
 | 
 | ||||||
|  |     def _remove_revisions(self, revisions=None): | ||||||
|  |         '''Remove old volume revisions. | ||||||
|  | 
 | ||||||
|  |         If no revisions list is given, it removes old revisions according to | ||||||
|  |         :py:attr:`revisions_to_keep` | ||||||
|  | 
 | ||||||
|  |         :param revisions: list of revisions to remove | ||||||
|  |         ''' | ||||||
|  |         if revisions is None: | ||||||
|  |             revisions = sorted(self.revisions.items(), | ||||||
|  |                 key=operator.itemgetter(1)) | ||||||
|  |             revisions = revisions[:-self.revisions_to_keep] | ||||||
|  |             revisions = [rev_id for rev_id, _ in revisions] | ||||||
|  | 
 | ||||||
|  |         for rev_id in revisions: | ||||||
|  |             try: | ||||||
|  |                 cmd = ['remove', self.vid + rev_id] | ||||||
|  |                 qubes_lvm(cmd, self.log) | ||||||
|  |             except qubes.storage.StoragePoolException: | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|     def _commit(self): |     def _commit(self): | ||||||
|         msg = "Trying to commit {!s}, but it has save_on_stop == False" |         msg = "Trying to commit {!s}, but it has save_on_stop == False" | ||||||
|         msg = msg.format(self) |         msg = msg.format(self) | ||||||
| @ -253,13 +247,11 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|         assert self.rw, msg |         assert self.rw, msg | ||||||
|         assert hasattr(self, '_vid_snap') |         assert hasattr(self, '_vid_snap') | ||||||
| 
 | 
 | ||||||
|         try: |         if self.revisions_to_keep > 0: | ||||||
|             cmd = ['remove', self.vid + "-back"] |             cmd = ['clone', self.vid, | ||||||
|             qubes_lvm(cmd, self.log) |                 '{}-{}-back'.format(self.vid, int(time.time()))] | ||||||
|         except qubes.storage.StoragePoolException: |  | ||||||
|             pass |  | ||||||
|         cmd = ['clone', self.vid, self.vid + "-back"] |  | ||||||
|             qubes_lvm(cmd, self.log) |             qubes_lvm(cmd, self.log) | ||||||
|  |             self._remove_revisions() | ||||||
| 
 | 
 | ||||||
|         cmd = ['remove', self.vid] |         cmd = ['remove', self.vid] | ||||||
|         qubes_lvm(cmd, self.log) |         qubes_lvm(cmd, self.log) | ||||||
| @ -290,6 +282,7 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|             cmd = ['remove', self._vid_snap] |             cmd = ['remove', self._vid_snap] | ||||||
|             qubes_lvm(cmd, self.log) |             qubes_lvm(cmd, self.log) | ||||||
| 
 | 
 | ||||||
|  |         self._remove_revisions(self.revisions.keys()) | ||||||
|         if not os.path.exists(self.path): |         if not os.path.exists(self.path): | ||||||
|             return |             return | ||||||
|         cmd = ['remove', self.vid] |         cmd = ['remove', self.vid] | ||||||
| @ -301,6 +294,7 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|         devpath = '/dev/' + self.vid |         devpath = '/dev/' + self.vid | ||||||
|         return devpath |         return devpath | ||||||
| 
 | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|     def import_volume(self, src_volume): |     def import_volume(self, src_volume): | ||||||
|         if not src_volume.save_on_stop: |         if not src_volume.save_on_stop: | ||||||
|             return self |             return self | ||||||
| @ -316,9 +310,14 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|             qubes_lvm(cmd, self.log) |             qubes_lvm(cmd, self.log) | ||||||
|         else: |         else: | ||||||
|             src_path = src_volume.export() |             src_path = src_volume.export() | ||||||
|             cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + self.vid, |             cmd = ['dd', 'if=' + src_path, 'of=/dev/' + self.vid, | ||||||
|                 'conv=sparse'] |                 'conv=sparse'] | ||||||
|             subprocess.check_call(cmd) |             p = yield from asyncio.create_subprocess_exec(*cmd) | ||||||
|  |             yield from p.wait() | ||||||
|  |             if p.returncode != 0: | ||||||
|  |                 raise qubes.storage.StoragePoolException( | ||||||
|  |                     'Failed to import volume {!r}, dd exit code: {}'.format( | ||||||
|  |                         src_volume, p.returncode)) | ||||||
|             reset_cache() |             reset_cache() | ||||||
| 
 | 
 | ||||||
|         return self |         return self | ||||||
| @ -330,18 +329,30 @@ class ThinVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
|     def is_dirty(self): |     def is_dirty(self): | ||||||
|         if self.save_on_stop: |         if self.save_on_stop: | ||||||
|             return os.path.exists(self.path + '-snap') |             return os.path.exists('/dev/' + self._vid_snap) | ||||||
|         return False |         return False | ||||||
| 
 | 
 | ||||||
|  |     def is_outdated(self): | ||||||
|  |         if not self.snap_on_start: | ||||||
|  |             return False | ||||||
|  |         if self._vid_snap not in size_cache: | ||||||
|  |             return False | ||||||
|  |         return (size_cache[self._vid_snap]['origin'] != | ||||||
|  |                self.source.vid.split('/')[1]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     def revert(self, revision=None): |     def revert(self, revision=None): | ||||||
|         old_path = self.path + '-back' |         if revision is None: | ||||||
|  |             revision = \ | ||||||
|  |                 max(self.revisions.items(), key=operator.itemgetter(1))[0] | ||||||
|  |         old_path = self.path + '-' + revision | ||||||
|         if not os.path.exists(old_path): |         if not os.path.exists(old_path): | ||||||
|             msg = "Volume {!s} has no {!s}".format(self, old_path) |             msg = "Volume {!s} has no {!s}".format(self, old_path) | ||||||
|             raise qubes.storage.StoragePoolException(msg) |             raise qubes.storage.StoragePoolException(msg) | ||||||
| 
 | 
 | ||||||
|         cmd = ['remove', self.vid] |         cmd = ['remove', self.vid] | ||||||
|         qubes_lvm(cmd, self.log) |         qubes_lvm(cmd, self.log) | ||||||
|         cmd = ['clone', self.vid + '-back', self.vid] |         cmd = ['clone', self.vid + '-' + revision, self.vid] | ||||||
|         qubes_lvm(cmd, self.log) |         qubes_lvm(cmd, self.log) | ||||||
|         reset_cache() |         reset_cache() | ||||||
|         return self |         return self | ||||||
| @ -381,22 +392,22 @@ class ThinVolume(qubes.storage.Volume): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def start(self): |     def start(self): | ||||||
|         if self.snap_on_start: |         if self.snap_on_start or self.save_on_stop: | ||||||
|             if not self.save_on_stop or not self.is_dirty(): |             if not self.save_on_stop or not self.is_dirty(): | ||||||
|                 self._snapshot() |                 self._snapshot() | ||||||
|         elif not self.save_on_stop: |         else: | ||||||
|             self._reset() |             self._reset() | ||||||
| 
 | 
 | ||||||
|         reset_cache() |         reset_cache() | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def stop(self): |     def stop(self): | ||||||
|         if self.save_on_stop and self.snap_on_start: |         if self.save_on_stop: | ||||||
|             self._commit() |             self._commit() | ||||||
|         if self.snap_on_start: |         if self.snap_on_start or self.save_on_stop: | ||||||
|             cmd = ['remove', self._vid_snap] |             cmd = ['remove', self._vid_snap] | ||||||
|             qubes_lvm(cmd, self.log) |             qubes_lvm(cmd, self.log) | ||||||
|         elif not self.save_on_stop: |         else: | ||||||
|             cmd = ['remove', self.vid] |             cmd = ['remove', self.vid] | ||||||
|             qubes_lvm(cmd, self.log) |             qubes_lvm(cmd, self.log) | ||||||
|         reset_cache() |         reset_cache() | ||||||
| @ -415,7 +426,7 @@ class ThinVolume(qubes.storage.Volume): | |||||||
|         ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in |         ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in | ||||||
|             the libvirt XML template as <disk>. |             the libvirt XML template as <disk>. | ||||||
|         ''' |         ''' | ||||||
|         if self.snap_on_start: |         if self.snap_on_start or self.save_on_stop: | ||||||
|             return qubes.storage.BlockDevice( |             return qubes.storage.BlockDevice( | ||||||
|                 '/dev/' + self._vid_snap, self.name, self.script, |                 '/dev/' + self._vid_snap, self.name, self.script, | ||||||
|                 self.rw, self.domain, self.devtype) |                 self.rw, self.domain, self.devtype) | ||||||
|  | |||||||
| @ -585,7 +585,7 @@ class SystemTestsMixin(object): | |||||||
|         # need some information from the real qubes.xml - at least installed |         # need some information from the real qubes.xml - at least installed | ||||||
|         # templates; should not be used for testing, only to initialize self.app |         # templates; should not be used for testing, only to initialize self.app | ||||||
|         self.host_app = qubes.Qubes(os.path.join( |         self.host_app = qubes.Qubes(os.path.join( | ||||||
|             qubes.config.system_path['qubes_base_dir'], |             qubes.config.qubes_base_dir, | ||||||
|             qubes.config.system_path['qubes_store_filename'])) |             qubes.config.system_path['qubes_store_filename'])) | ||||||
|         if os.path.exists(CLASS_XMLPATH): |         if os.path.exists(CLASS_XMLPATH): | ||||||
|             shutil.copy(CLASS_XMLPATH, XMLPATH) |             shutil.copy(CLASS_XMLPATH, XMLPATH) | ||||||
| @ -729,7 +729,7 @@ class SystemTestsMixin(object): | |||||||
|                 'qubes_appvms_dir', |                 'qubes_appvms_dir', | ||||||
|                 'qubes_servicevms_dir', |                 'qubes_servicevms_dir', | ||||||
|                 'qubes_templates_dir'): |                 'qubes_templates_dir'): | ||||||
|             dirpath = os.path.join(qubes.config.system_path['qubes_base_dir'], |             dirpath = os.path.join(qubes.config.qubes_base_dir, | ||||||
|                 qubes.config.system_path[dirspec], vmname) |                 qubes.config.system_path[dirspec], vmname) | ||||||
|             if os.path.exists(dirpath): |             if os.path.exists(dirpath): | ||||||
|                 if os.path.isdir(dirpath): |                 if os.path.isdir(dirpath): | ||||||
| @ -793,7 +793,7 @@ class SystemTestsMixin(object): | |||||||
|                 'qubes_appvms_dir', |                 'qubes_appvms_dir', | ||||||
|                 'qubes_servicevms_dir', |                 'qubes_servicevms_dir', | ||||||
|                 'qubes_templates_dir'): |                 'qubes_templates_dir'): | ||||||
|             dirpath = os.path.join(qubes.config.system_path['qubes_base_dir'], |             dirpath = os.path.join(qubes.config.qubes_base_dir, | ||||||
|                 qubes.config.system_path[dirspec]) |                 qubes.config.system_path[dirspec]) | ||||||
|             for name in os.listdir(dirpath): |             for name in os.listdir(dirpath): | ||||||
|                 if name.startswith(prefix): |                 if name.startswith(prefix): | ||||||
|  | |||||||
| @ -37,7 +37,7 @@ import qubes.storage | |||||||
| 
 | 
 | ||||||
| # properties defined in API | # properties defined in API | ||||||
| volume_properties = [ | volume_properties = [ | ||||||
|     'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source', |     'pool', 'vid', 'size', 'usage', 'rw', 'source', | ||||||
|     'save_on_stop', 'snap_on_start'] |     'save_on_stop', 'snap_on_start'] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -77,68 +77,6 @@ class TC_01_Properties(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase): | |||||||
|                                       label='red') |                                       label='red') | ||||||
|         self.loop.run_until_complete(self.vm.create_on_disk()) |         self.loop.run_until_complete(self.vm.create_on_disk()) | ||||||
| 
 | 
 | ||||||
|     @unittest.expectedFailure |  | ||||||
|     def test_000_rename(self): |  | ||||||
|         newname = self.make_vm_name('newname') |  | ||||||
| 
 |  | ||||||
|         self.assertEqual(self.vm.name, self.vmname) |  | ||||||
|         self.vm.firewall.policy = 'drop' |  | ||||||
|         self.vm.firewall.rules = [ |  | ||||||
|             qubes.firewall.Rule(None, action='accept', specialtarget='dns') |  | ||||||
|         ] |  | ||||||
|         self.vm.firewall.save() |  | ||||||
|         self.vm.autostart = True |  | ||||||
|         self.addCleanup(os.system, |  | ||||||
|                         'sudo systemctl -q disable qubes-vm@{}.service || :'. |  | ||||||
|                         format(self.vmname)) |  | ||||||
|         pre_rename_firewall = self.vm.firewall.rules |  | ||||||
| 
 |  | ||||||
|         with self.assertNotRaises( |  | ||||||
|                 (OSError, libvirt.libvirtError, qubes.exc.QubesException)): |  | ||||||
|             self.vm.name = newname |  | ||||||
|         self.assertEqual(self.vm.name, newname) |  | ||||||
|         self.assertEqual(self.vm.dir_path, |  | ||||||
|             os.path.join( |  | ||||||
|                 qubes.config.system_path['qubes_base_dir'], |  | ||||||
|                 qubes.config.system_path['qubes_appvms_dir'], newname)) |  | ||||||
|         self.assertTrue(os.path.exists( |  | ||||||
|             os.path.join(self.vm.dir_path, "apps", newname + "-vm.directory"))) |  | ||||||
|         # FIXME: set whitelisted-appmenus.list first |  | ||||||
|         self.assertTrue(os.path.exists(os.path.join( |  | ||||||
|             self.vm.dir_path, "apps", newname + "-firefox.desktop"))) |  | ||||||
|         self.assertTrue(os.path.exists( |  | ||||||
|             os.path.join(os.getenv("HOME"), ".local/share/desktop-directories", |  | ||||||
|                 newname + "-vm.directory"))) |  | ||||||
|         self.assertTrue(os.path.exists( |  | ||||||
|             os.path.join(os.getenv("HOME"), ".local/share/applications", |  | ||||||
|                 newname + "-firefox.desktop"))) |  | ||||||
|         self.assertFalse(os.path.exists( |  | ||||||
|             os.path.join(os.getenv("HOME"), ".local/share/desktop-directories", |  | ||||||
|                 self.vmname + "-vm.directory"))) |  | ||||||
|         self.assertFalse(os.path.exists( |  | ||||||
|             os.path.join(os.getenv("HOME"), ".local/share/applications", |  | ||||||
|                 self.vmname + "-firefox.desktop"))) |  | ||||||
|         self.vm.firewall.load() |  | ||||||
|         self.assertEqual(pre_rename_firewall, self.vm.firewall.rules) |  | ||||||
|         with self.assertNotRaises((qubes.exc.QubesException, OSError)): |  | ||||||
|             self.vm.firewall.save() |  | ||||||
|         self.assertTrue(self.vm.autostart) |  | ||||||
|         self.assertTrue(os.path.exists( |  | ||||||
|             '/etc/systemd/system/multi-user.target.wants/' |  | ||||||
|             'qubes-vm@{}.service'.format(newname))) |  | ||||||
|         self.assertFalse(os.path.exists( |  | ||||||
|             '/etc/systemd/system/multi-user.target.wants/' |  | ||||||
|             'qubes-vm@{}.service'.format(self.vmname))) |  | ||||||
| 
 |  | ||||||
|     def test_001_rename_libvirt_undefined(self): |  | ||||||
|         self.vm.libvirt_domain.undefine() |  | ||||||
|         self.vm._libvirt_domain = None  # pylint: disable=protected-access |  | ||||||
| 
 |  | ||||||
|         newname = self.make_vm_name('newname') |  | ||||||
|         with self.assertNotRaises( |  | ||||||
|                 (OSError, libvirt.libvirtError, qubes.exc.QubesException)): |  | ||||||
|             self.vm.name = newname |  | ||||||
| 
 |  | ||||||
|     @unittest.expectedFailure |     @unittest.expectedFailure | ||||||
|     def test_030_clone(self): |     def test_030_clone(self): | ||||||
|         testvm1 = self.app.add_new_vm( |         testvm1 = self.app.add_new_vm( | ||||||
|  | |||||||
| @ -59,7 +59,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'save_on_stop': False, |             'save_on_stop': False, | ||||||
|             'rw': True, |             'rw': True, | ||||||
|         } |         } | ||||||
| @ -91,7 +90,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'save_on_stop': True, |             'save_on_stop': True, | ||||||
|             'rw': True, |             'rw': True, | ||||||
|         } |         } | ||||||
| @ -126,7 +124,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'save_on_stop': False, |             'save_on_stop': False, | ||||||
|             'rw': False, |             'rw': False, | ||||||
|         } |         } | ||||||
| @ -157,7 +154,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'save_on_stop': True, |             'save_on_stop': True, | ||||||
|             'rw': True, |             'rw': True, | ||||||
|         } |         } | ||||||
| @ -166,7 +162,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'snap_on_start': True, |             'snap_on_start': True, | ||||||
|             'source': testvol.vid, |             'source': testvol.vid, | ||||||
|             'rw': True, |             'rw': True, | ||||||
| @ -225,7 +220,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'save_on_stop': True, |             'save_on_stop': True, | ||||||
|             'rw': True, |             'rw': True, | ||||||
|         } |         } | ||||||
| @ -234,7 +228,6 @@ class StorageTestMixin(qubes.tests.SystemTestsMixin): | |||||||
|         volume_config = { |         volume_config = { | ||||||
|             'pool': self.pool.name, |             'pool': self.pool.name, | ||||||
|             'size': size, |             'size': size, | ||||||
|             'internal': False, |  | ||||||
|             'snap_on_start': True, |             'snap_on_start': True, | ||||||
|             'source': testvol.vid, |             'source': testvol.vid, | ||||||
|             'rw': True, |             'rw': True, | ||||||
|  | |||||||
| @ -136,6 +136,15 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase): | |||||||
|         self.assertFalse(volume.snap_on_start) |         self.assertFalse(volume.snap_on_start) | ||||||
|         self.assertTrue(volume.save_on_stop) |         self.assertTrue(volume.save_on_stop) | ||||||
|         self.assertTrue(volume.rw) |         self.assertTrue(volume.rw) | ||||||
|  |         block = volume.block_device() | ||||||
|  |         self.assertEqual(block.path, | ||||||
|  |             '{base}.img:{base}-cow.img'.format( | ||||||
|  |                 base=self.POOL_DIR + '/appvms/' + vm.name + '/root')) | ||||||
|  |         self.assertEqual(block.script, 'block-origin') | ||||||
|  |         self.assertEqual(block.rw, True) | ||||||
|  |         self.assertEqual(block.name, 'root') | ||||||
|  |         self.assertEqual(block.devtype, 'disk') | ||||||
|  |         self.assertIsNone(block.domain) | ||||||
| 
 | 
 | ||||||
|     def test_001_snapshot_volume(self): |     def test_001_snapshot_volume(self): | ||||||
|         template_vm = self.app.default_template |         template_vm = self.app.default_template | ||||||
| @ -169,6 +178,19 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase): | |||||||
|         self.assertFalse(volume.save_on_stop) |         self.assertFalse(volume.save_on_stop) | ||||||
|         self.assertFalse(volume.rw) |         self.assertFalse(volume.rw) | ||||||
|         self.assertEqual(volume.usage, 0) |         self.assertEqual(volume.usage, 0) | ||||||
|  |         block = volume.block_device() | ||||||
|  |         assert isinstance(block, qubes.storage.BlockDevice) | ||||||
|  |         self.assertEqual(block.path, | ||||||
|  |             '{base}/{src}.img:{base}/{src}-cow.img:' | ||||||
|  |             '{base}/{dst}-cow.img'.format( | ||||||
|  |                 base=self.POOL_DIR, | ||||||
|  |                 src='vm-templates/' + template_vm.name + '/root', | ||||||
|  |                 dst='appvms/' + vm.name + '/root', | ||||||
|  |         )) | ||||||
|  |         self.assertEqual(block.name, 'root') | ||||||
|  |         self.assertEqual(block.script, 'block-snapshot') | ||||||
|  |         self.assertEqual(block.rw, False) | ||||||
|  |         self.assertEqual(block.devtype, 'disk') | ||||||
| 
 | 
 | ||||||
|     def test_002_read_write_volume(self): |     def test_002_read_write_volume(self): | ||||||
|         config = { |         config = { | ||||||
| @ -186,6 +208,12 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase): | |||||||
|         self.assertFalse(volume.snap_on_start) |         self.assertFalse(volume.snap_on_start) | ||||||
|         self.assertTrue(volume.save_on_stop) |         self.assertTrue(volume.save_on_stop) | ||||||
|         self.assertTrue(volume.rw) |         self.assertTrue(volume.rw) | ||||||
|  |         block = volume.block_device() | ||||||
|  |         self.assertEqual(block.name, 'root') | ||||||
|  |         self.assertEqual(block.path, '{base}.img:{base}-cow.img'.format( | ||||||
|  |             base=self.POOL_DIR + '/appvms/' + vm.name + '/root')) | ||||||
|  |         self.assertEqual(block.rw, True) | ||||||
|  |         self.assertEqual(block.script, 'block-origin') | ||||||
| 
 | 
 | ||||||
|     def test_003_read_only_volume(self): |     def test_003_read_only_volume(self): | ||||||
|         template = self.app.default_template |         template = self.app.default_template | ||||||
| @ -207,6 +235,8 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase): | |||||||
|         self.assertFalse(volume.snap_on_start) |         self.assertFalse(volume.snap_on_start) | ||||||
|         self.assertFalse(volume.save_on_stop) |         self.assertFalse(volume.save_on_stop) | ||||||
|         self.assertFalse(volume.rw) |         self.assertFalse(volume.rw) | ||||||
|  |         block = volume.block_device() | ||||||
|  |         self.assertEqual(block.rw, False) | ||||||
| 
 | 
 | ||||||
|     def test_004_volatile_volume(self): |     def test_004_volatile_volume(self): | ||||||
|         config = { |         config = { | ||||||
| @ -248,7 +278,8 @@ class TC_01_FileVolumes(qubes.tests.QubesTestCase): | |||||||
| 
 | 
 | ||||||
|         expected = vm.dir_path + '/root.img:' + vm.dir_path + '/root-cow.img' |         expected = vm.dir_path + '/root.img:' + vm.dir_path + '/root-cow.img' | ||||||
|         self.assertVolumePath(vm, 'root', expected, rw=True) |         self.assertVolumePath(vm, 'root', expected, rw=True) | ||||||
|         expected = vm.dir_path + '/private.img' |         expected = vm.dir_path + '/private.img:' + \ | ||||||
|  |                    vm.dir_path + '/private-cow.img' | ||||||
|         self.assertVolumePath(vm, 'private', expected, rw=True) |         self.assertVolumePath(vm, 'private', expected, rw=True) | ||||||
|         expected = vm.dir_path + '/volatile.img' |         expected = vm.dir_path + '/volatile.img' | ||||||
|         self.assertVolumePath(vm, 'volatile', expected, rw=True) |         self.assertVolumePath(vm, 'volatile', expected, rw=True) | ||||||
| @ -275,8 +306,8 @@ class TC_03_FilePool(qubes.tests.QubesTestCase): | |||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         """ Add a test file based storage pool """ |         """ Add a test file based storage pool """ | ||||||
|         super(TC_03_FilePool, self).setUp() |         super(TC_03_FilePool, self).setUp() | ||||||
|         self._orig_qubes_base_dir = qubes.config.system_path['qubes_base_dir'] |         self._orig_qubes_base_dir = qubes.config.qubes_base_dir | ||||||
|         qubes.config.system_path['qubes_base_dir'] = '/tmp/qubes-test' |         qubes.config.qubes_base_dir = '/tmp/qubes-test' | ||||||
|         self.app = TestApp() |         self.app = TestApp() | ||||||
|         self.app.create_dummy_template() |         self.app.create_dummy_template() | ||||||
|         self.app.add_pool(**self.POOL_CONFIG) |         self.app.add_pool(**self.POOL_CONFIG) | ||||||
| @ -289,7 +320,7 @@ class TC_03_FilePool(qubes.tests.QubesTestCase): | |||||||
|         shutil.rmtree(self.POOL_DIR, ignore_errors=True) |         shutil.rmtree(self.POOL_DIR, ignore_errors=True) | ||||||
|         if os.path.exists('/tmp/qubes-test'): |         if os.path.exists('/tmp/qubes-test'): | ||||||
|             shutil.rmtree('/tmp/qubes-test') |             shutil.rmtree('/tmp/qubes-test') | ||||||
|         qubes.config.system_path['qubes_base_dir'] = self._orig_qubes_base_dir |         qubes.config.qubes_base_dir = self._orig_qubes_base_dir | ||||||
| 
 | 
 | ||||||
|     def test_001_pool_exists(self): |     def test_001_pool_exists(self): | ||||||
|         """ Check if the storage pool was added to the storage pool config """ |         """ Check if the storage pool was added to the storage pool config """ | ||||||
|  | |||||||
| @ -80,50 +80,6 @@ class TC_00_setters(qubes.tests.QubesTestCase): | |||||||
|             qubes.vm.qubesvm._setter_qid(self.vm, |             qubes.vm.qubesvm._setter_qid(self.vm, | ||||||
|                 self.prop, qubes.config.max_qid + 5) |                 self.prop, qubes.config.max_qid + 5) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     def test_010_setter_name(self): |  | ||||||
|         self.assertEqual( |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'test_name-1'), |  | ||||||
|             'test_name-1') |  | ||||||
| 
 |  | ||||||
|     def test_011_setter_name_not_a_string(self): |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         with self.assertRaises(TypeError): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, False) |  | ||||||
| 
 |  | ||||||
|     def test_012_setter_name_longer_than_31(self): |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         with self.assertRaises(ValueError): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, 't' * 32) |  | ||||||
| 
 |  | ||||||
|     def test_013_setter_name_illegal_character(self): |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         with self.assertRaises(ValueError): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'test#') |  | ||||||
| 
 |  | ||||||
|     def test_014_setter_name_first_not_letter(self): |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         with self.assertRaises(ValueError): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, '1test') |  | ||||||
| 
 |  | ||||||
|     def test_015_setter_name_running(self): |  | ||||||
|         self.vm.running = True |  | ||||||
|         with self.assertRaises(qubes.exc.QubesVMNotHaltedError): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'testname') |  | ||||||
| 
 |  | ||||||
|     def test_016_setter_name_installed_by_rpm(self): |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         self.vm.installed_by_rpm = True |  | ||||||
|         with self.assertRaises(qubes.exc.QubesException): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'testname') |  | ||||||
| 
 |  | ||||||
|     def test_017_setter_name_duplicate(self): |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         self.vm.app.domains['duplicate'] = TestVM(name='duplicate') |  | ||||||
|         with self.assertRaises(qubes.exc.QubesException): |  | ||||||
|             qubes.vm.qubesvm._setter_name(self.vm, self.prop, 'duplicate') |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     @unittest.skip('test not implemented') |     @unittest.skip('test not implemented') | ||||||
|     def test_020_setter_kernel(self): |     def test_020_setter_kernel(self): | ||||||
|         pass |         pass | ||||||
|  | |||||||
| @ -49,7 +49,6 @@ class AppVM(qubes.vm.qubesvm.QubesVM): | |||||||
|                 'save_on_stop': False, |                 'save_on_stop': False, | ||||||
|                 'rw': False, |                 'rw': False, | ||||||
|                 'source': None, |                 'source': None, | ||||||
|                 'internal': True |  | ||||||
|             }, |             }, | ||||||
|             'private': { |             'private': { | ||||||
|                 'name': 'private', |                 'name': 'private', | ||||||
| @ -58,20 +57,21 @@ class AppVM(qubes.vm.qubesvm.QubesVM): | |||||||
|                 'save_on_stop': True, |                 'save_on_stop': True, | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|                 'size': defaults['private_img_size'], |                 'size': defaults['private_img_size'], | ||||||
|                 'internal': True |  | ||||||
|             }, |             }, | ||||||
|             'volatile': { |             'volatile': { | ||||||
|                 'name': 'volatile', |                 'name': 'volatile', | ||||||
|                 'pool': 'default', |                 'pool': 'default', | ||||||
|  |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'size': defaults['root_img_size'], |                 'size': defaults['root_img_size'], | ||||||
|                 'internal': True, |  | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|             }, |             }, | ||||||
|             'kernel': { |             'kernel': { | ||||||
|                 'name': 'kernel', |                 'name': 'kernel', | ||||||
|                 'pool': 'linux-kernel', |                 'pool': 'linux-kernel', | ||||||
|  |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': False, |                 'rw': False, | ||||||
|                 'internal': True |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -47,20 +47,19 @@ class DispVM(qubes.vm.qubesvm.QubesVM): | |||||||
|                 'snap_on_start': True, |                 'snap_on_start': True, | ||||||
|                 'save_on_stop': False, |                 'save_on_stop': False, | ||||||
|                 'rw': False, |                 'rw': False, | ||||||
|                 'internal': True |  | ||||||
|             }, |             }, | ||||||
|             'private': { |             'private': { | ||||||
|                 'name': 'private', |                 'name': 'private', | ||||||
|                 'pool': 'default', |                 'pool': 'default', | ||||||
|                 'snap_on_start': True, |                 'snap_on_start': True, | ||||||
|                 'save_on_stop': False, |                 'save_on_stop': False, | ||||||
|                 'internal': True, |  | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|             }, |             }, | ||||||
|             'volatile': { |             'volatile': { | ||||||
|                 'name': 'volatile', |                 'name': 'volatile', | ||||||
|                 'pool': 'default', |                 'pool': 'default', | ||||||
|                 'internal': True, |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|                 'size': qubes.config.defaults['root_img_size'] + |                 'size': qubes.config.defaults['root_img_size'] + | ||||||
|                         qubes.config.defaults['private_img_size'], |                         qubes.config.defaults['private_img_size'], | ||||||
| @ -68,8 +67,9 @@ class DispVM(qubes.vm.qubesvm.QubesVM): | |||||||
|             'kernel': { |             'kernel': { | ||||||
|                 'name': 'kernel', |                 'name': 'kernel', | ||||||
|                 'pool': 'linux-kernel', |                 'pool': 'linux-kernel', | ||||||
|  |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': False, |                 'rw': False, | ||||||
|                 'internal': True |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if 'name' not in kwargs and 'dispid' in kwargs: |         if 'name' not in kwargs and 'dispid' in kwargs: | ||||||
|  | |||||||
| @ -72,28 +72,6 @@ def _setter_qid(self, prop, value): | |||||||
|     return value |     return value | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _setter_name(self, prop, value): |  | ||||||
|     ''' Helper for setting the domain name ''' |  | ||||||
|     qubes.vm.validate_name(self, prop, value) |  | ||||||
| 
 |  | ||||||
|     if self.is_running(): |  | ||||||
|         raise qubes.exc.QubesVMNotHaltedError( |  | ||||||
|             self, 'Cannot change name of running VM') |  | ||||||
| 
 |  | ||||||
|     try: |  | ||||||
|         if self.installed_by_rpm: |  | ||||||
|             raise qubes.exc.QubesException('Cannot rename VM installed by RPM ' |  | ||||||
|                 '-- first clone VM and then use yum to remove package.') |  | ||||||
|     except AttributeError: |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     if value in self.app.domains: |  | ||||||
|         raise qubes.exc.QubesPropertyValueError(self, prop, value, |  | ||||||
|             'VM named {} alread exists'.format(value)) |  | ||||||
| 
 |  | ||||||
|     return value |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _setter_kernel(self, prop, value): | def _setter_kernel(self, prop, value): | ||||||
|     ''' Helper for setting the domain kernel and running sanity checks on it. |     ''' Helper for setting the domain kernel and running sanity checks on it. | ||||||
|     '''  # pylint: disable=unused-argument |     '''  # pylint: disable=unused-argument | ||||||
| @ -615,7 +593,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | |||||||
|     def dir_path(self): |     def dir_path(self): | ||||||
|         '''Root directory for files related to this domain''' |         '''Root directory for files related to this domain''' | ||||||
|         return os.path.join( |         return os.path.join( | ||||||
|             qubes.config.system_path['qubes_base_dir'], |             qubes.config.qubes_base_dir, | ||||||
|             self.dir_path_prefix, |             self.dir_path_prefix, | ||||||
|             self.name) |             self.name) | ||||||
| 
 | 
 | ||||||
| @ -748,33 +726,13 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | |||||||
|             else: |             else: | ||||||
|                 shutil.copy(newvalue.icon_path, self.icon_path) |                 shutil.copy(newvalue.icon_path, self.icon_path) | ||||||
| 
 | 
 | ||||||
|     @qubes.events.handler('property-pre-set:name') |  | ||||||
|     def on_property_pre_set_name(self, event, name, newvalue, oldvalue=None): |  | ||||||
|         # pylint: disable=unused-argument |  | ||||||
|         try: |  | ||||||
|             self.app.domains[newvalue] |  | ||||||
|         except KeyError: |  | ||||||
|             pass |  | ||||||
|         else: |  | ||||||
|             raise qubes.exc.QubesValueError( |  | ||||||
|                 'VM named {!r} already exists'.format(newvalue)) |  | ||||||
| 
 |  | ||||||
|         # TODO not self.is_stopped() would be more appropriate |  | ||||||
|         if self.is_running(): |  | ||||||
|             raise qubes.exc.QubesVMNotHaltedError( |  | ||||||
|                 'Cannot change name of running domain {!r}'.format(oldvalue)) |  | ||||||
| 
 |  | ||||||
|         if self.autostart: |  | ||||||
|             subprocess.check_call(['sudo', 'systemctl', '-q', 'disable', |  | ||||||
|                                    'qubes-vm@{}.service'.format(oldvalue)]) |  | ||||||
| 
 |  | ||||||
|     @qubes.events.handler('property-pre-set:kernel') |     @qubes.events.handler('property-pre-set:kernel') | ||||||
|     def on_property_pre_set_kernel(self, event, name, newvalue, oldvalue=None): |     def on_property_pre_set_kernel(self, event, name, newvalue, oldvalue=None): | ||||||
|         # pylint: disable=unused-argument |         # pylint: disable=unused-argument | ||||||
|         if not newvalue: |         if not newvalue: | ||||||
|             return |             return | ||||||
|         dirname = os.path.join( |         dirname = os.path.join( | ||||||
|             qubes.config.system_path['qubes_base_dir'], |             qubes.config.qubes_base_dir, | ||||||
|             qubes.config.system_path['qubes_kernels_base_dir'], |             qubes.config.system_path['qubes_kernels_base_dir'], | ||||||
|             newvalue) |             newvalue) | ||||||
|         if not os.path.exists(dirname): |         if not os.path.exists(dirname): | ||||||
| @ -788,29 +746,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | |||||||
|                     'Kernel {!r} not properly installed: ' |                     'Kernel {!r} not properly installed: ' | ||||||
|                     'missing {!r} file'.format(newvalue, filename)) |                     'missing {!r} file'.format(newvalue, filename)) | ||||||
| 
 | 
 | ||||||
|     @qubes.events.handler('property-set:name') |  | ||||||
|     def on_property_set_name(self, event, name, newvalue, oldvalue=None): |  | ||||||
|         # pylint: disable=unused-argument |  | ||||||
|         self.init_log() |  | ||||||
| 
 |  | ||||||
|         old_dir_path = os.path.join(os.path.dirname(self.dir_path), oldvalue) |  | ||||||
|         new_dir_path = os.path.join(os.path.dirname(self.dir_path), newvalue) |  | ||||||
|         os.rename(old_dir_path, new_dir_path) |  | ||||||
| 
 |  | ||||||
|         self.storage.rename(oldvalue, newvalue) |  | ||||||
| 
 |  | ||||||
|         if self._libvirt_domain is not None: |  | ||||||
|             self.libvirt_domain.undefine() |  | ||||||
|             self._libvirt_domain = None |  | ||||||
|         if self._qdb_connection is not None: |  | ||||||
|             self._qdb_connection.close() |  | ||||||
|             self._qdb_connection = None |  | ||||||
| 
 |  | ||||||
|         self._update_libvirt_domain() |  | ||||||
| 
 |  | ||||||
|         if self.autostart: |  | ||||||
|             self.autostart = self.autostart |  | ||||||
| 
 |  | ||||||
|     @qubes.events.handler('property-pre-set:autostart') |     @qubes.events.handler('property-pre-set:autostart') | ||||||
|     def on_property_pre_set_autostart(self, event, name, newvalue, |     def on_property_pre_set_autostart(self, event, name, newvalue, | ||||||
|             oldvalue=None): |             oldvalue=None): | ||||||
| @ -1728,45 +1663,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | |||||||
| 
 | 
 | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|     def is_outdated(self): |  | ||||||
|         '''Check whether domain needs restart to update root image from \ |  | ||||||
|             template. |  | ||||||
| 
 |  | ||||||
|         :returns: :py:obj:`True` if is outdated, :py:obj:`False` otherwise. |  | ||||||
|         :rtype: bool |  | ||||||
|         ''' |  | ||||||
|         # pylint: disable=no-member |  | ||||||
| 
 |  | ||||||
|         # Makes sense only on VM based on template |  | ||||||
|         if self.template is None: |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|         if not self.is_running(): |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|         if not hasattr(self.template, 'rootcow_img'): |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|         rootimg_inode = os.stat(self.template.root_img) |  | ||||||
|         try: |  | ||||||
|             rootcow_inode = os.stat(self.template.rootcow_img) |  | ||||||
|         except OSError: |  | ||||||
|             # The only case when rootcow_img doesn't exists is in the middle of |  | ||||||
|             # commit_changes, so VM is outdated right now |  | ||||||
|             return True |  | ||||||
| 
 |  | ||||||
|         current_dmdev = "/dev/mapper/snapshot-{0:x}:{1}-{2:x}:{3}".format( |  | ||||||
|                 rootimg_inode[2], rootimg_inode[1], |  | ||||||
|                 rootcow_inode[2], rootcow_inode[1]) |  | ||||||
| 
 |  | ||||||
|         # FIXME |  | ||||||
|         # 51712 (0xCA00) is xvda |  | ||||||
|         #  backend node name not available through xenapi :( |  | ||||||
|         used_dmdev = self.app.vmm.xs.read('', |  | ||||||
|             '/local/domain/0/backend/vbd/{}/51712/node'.format(self.xid)) |  | ||||||
| 
 |  | ||||||
|         return used_dmdev != current_dmdev |  | ||||||
| 
 |  | ||||||
|     # |     # | ||||||
|     # helper methods |     # helper methods | ||||||
|     # |     # | ||||||
| @ -1900,7 +1796,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _clean_volume_config(config): | def _clean_volume_config(config): | ||||||
|     common_attributes = ['name', 'pool', 'size', 'internal', 'removable', |     common_attributes = ['name', 'pool', 'size', | ||||||
|                          'revisions_to_keep', 'rw', 'snap_on_start', |                          'revisions_to_keep', 'rw', 'snap_on_start', | ||||||
|                          'save_on_stop', 'source'] |                          'save_on_stop', 'source'] | ||||||
|     config_copy = copy.deepcopy(config) |     config_copy = copy.deepcopy(config) | ||||||
| @ -1909,20 +1805,17 @@ def _clean_volume_config(config): | |||||||
| 
 | 
 | ||||||
| def _patch_pool_config(config, pool=None, pools=None): | def _patch_pool_config(config, pool=None, pools=None): | ||||||
|     assert pool is not None or pools is not None |     assert pool is not None or pools is not None | ||||||
|     is_saveable = 'save_on_stop' in config and config['save_on_stop'] |     is_snapshot = config['snap_on_start'] | ||||||
|     is_resetable = not ('snap_on_start' in config and  # volatile |     is_rw = config['rw'] | ||||||
|                         config['snap_on_start'] and not is_saveable) |  | ||||||
| 
 |  | ||||||
|     is_exportable = is_saveable or is_resetable |  | ||||||
| 
 | 
 | ||||||
|     name = config['name'] |     name = config['name'] | ||||||
| 
 | 
 | ||||||
|     if pool and is_exportable and config['pool'] == 'default': |     if pool and not is_snapshot and is_rw: | ||||||
|         config['pool'] = str(pool) |         config['pool'] = str(pool) | ||||||
|     elif pool and not is_exportable: |     elif pool: | ||||||
|         pass |         pass | ||||||
|     elif pools and name in pools.keys(): |     elif pools and name in pools.keys(): | ||||||
|         if is_exportable: |         if not is_snapshot: | ||||||
|             config['pool'] = str(pools[name]) |             config['pool'] = str(pools[name]) | ||||||
|         else: |         else: | ||||||
|             msg = "Can't clone a snapshot volume {!s} to pool {!s} " \ |             msg = "Can't clone a snapshot volume {!s} to pool {!s} " \ | ||||||
|  | |||||||
| @ -35,7 +35,6 @@ class StandaloneVM(qubes.vm.qubesvm.QubesVM): | |||||||
|                 'save_on_stop': True, |                 'save_on_stop': True, | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|                 'source': None, |                 'source': None, | ||||||
|                 'internal': True, |  | ||||||
|                 'size': qubes.config.defaults['root_img_size'], |                 'size': qubes.config.defaults['root_img_size'], | ||||||
|             }, |             }, | ||||||
|             'private': { |             'private': { | ||||||
| @ -45,21 +44,22 @@ class StandaloneVM(qubes.vm.qubesvm.QubesVM): | |||||||
|                 'save_on_stop': True, |                 'save_on_stop': True, | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|                 'source': None, |                 'source': None, | ||||||
|                 'internal': True, |  | ||||||
|                 'size': qubes.config.defaults['private_img_size'], |                 'size': qubes.config.defaults['private_img_size'], | ||||||
|             }, |             }, | ||||||
|             'volatile': { |             'volatile': { | ||||||
|                 'name': 'volatile', |                 'name': 'volatile', | ||||||
|                 'pool': 'default', |                 'pool': 'default', | ||||||
|                 'internal': True, |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|                 'size': qubes.config.defaults['root_img_size'], |                 'size': qubes.config.defaults['root_img_size'], | ||||||
|             }, |             }, | ||||||
|             'kernel': { |             'kernel': { | ||||||
|                 'name': 'kernel', |                 'name': 'kernel', | ||||||
|                 'pool': 'linux-kernel', |                 'pool': 'linux-kernel', | ||||||
|  |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': False, |                 'rw': False, | ||||||
|                 'internal': True |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         super(StandaloneVM, self).__init__(*args, **kwargs) |         super(StandaloneVM, self).__init__(*args, **kwargs) | ||||||
|  | |||||||
| @ -71,7 +71,6 @@ class TemplateVM(QubesVM): | |||||||
|                 'rw': True, |                 'rw': True, | ||||||
|                 'source': None, |                 'source': None, | ||||||
|                 'size': defaults['root_img_size'], |                 'size': defaults['root_img_size'], | ||||||
|                 'internal': True |  | ||||||
|             }, |             }, | ||||||
|             'private': { |             'private': { | ||||||
|                 'name': 'private', |                 'name': 'private', | ||||||
| @ -82,30 +81,21 @@ class TemplateVM(QubesVM): | |||||||
|                 'source': None, |                 'source': None, | ||||||
|                 'size': defaults['private_img_size'], |                 'size': defaults['private_img_size'], | ||||||
|                 'revisions_to_keep': 0, |                 'revisions_to_keep': 0, | ||||||
|                 'internal': True |  | ||||||
|             }, |             }, | ||||||
|             'volatile': { |             'volatile': { | ||||||
|                 'name': 'volatile', |                 'name': 'volatile', | ||||||
|                 'pool': 'default', |                 'pool': 'default', | ||||||
|                 'size': defaults['root_img_size'], |                 'size': defaults['root_img_size'], | ||||||
|                 'internal': True, |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': True, |                 'rw': True, | ||||||
|             }, |             }, | ||||||
|             'kernel': { |             'kernel': { | ||||||
|                 'name': 'kernel', |                 'name': 'kernel', | ||||||
|                 'pool': 'linux-kernel', |                 'pool': 'linux-kernel', | ||||||
|                 'internal': True, |                 'snap_on_start': False, | ||||||
|  |                 'save_on_stop': False, | ||||||
|                 'rw': False |                 'rw': False | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         super(TemplateVM, self).__init__(*args, **kwargs) |         super(TemplateVM, self).__init__(*args, **kwargs) | ||||||
| 
 |  | ||||||
|     def commit_changes(self): |  | ||||||
|         '''Commit changes to template''' |  | ||||||
|         self.log.debug('commit_changes()') |  | ||||||
| 
 |  | ||||||
|         if not self.app.vmm.offline_mode: |  | ||||||
|             assert not self.is_running(), \ |  | ||||||
|                 'Attempt to commit changes on running Template VM!' |  | ||||||
| 
 |  | ||||||
|         self.storage.commit() |  | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marek Marczykowski-Górecki
						Marek Marczykowski-Górecki