Sfoglia il codice sorgente

Merge branch 'core3-devel-20170619'

Marek Marczykowski-Górecki 7 anni fa
parent
commit
4208a98bd7
5 ha cambiato i file con 243 aggiunte e 35 eliminazioni
  1. 61 0
      qubes/api/admin.py
  2. 10 1
      qubes/exc.py
  3. 31 34
      qubes/storage/__init__.py
  4. 136 0
      qubes/tests/api_admin.py
  5. 5 0
      qubes/vm/__init__.py

+ 61 - 0
qubes/api/admin.py

@@ -293,6 +293,24 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.dest.storage.get_pool(volume).revert(revision)
         self.app.save()
 
+    @qubes.api.method('admin.vm.volume.Clone')
+    @asyncio.coroutine
+    def vm_volume_clone(self, untrusted_payload):
+        assert self.arg in self.dest.volumes.keys()
+        untrusted_target = untrusted_payload.decode('ascii').strip()
+        del untrusted_payload
+        qubes.vm.validate_name(None, None, untrusted_target)
+        target_vm = self.app.domains[untrusted_target]
+        del untrusted_target
+        assert self.arg in target_vm.volumes.keys()
+
+        volume = self.dest.volumes[self.arg]
+
+        self.fire_event_for_permission(target_vm=target_vm, volume=volume)
+
+        yield from target_vm.storage.clone_volume(self.dest, self.arg)
+        self.app.save()
+
     @qubes.api.method('admin.vm.volume.Resize')
     @asyncio.coroutine
     def vm_volume_resize(self, untrusted_payload):
@@ -338,6 +356,49 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
         return '{} {}'.format(size, path)
 
+    @qubes.api.method('admin.vm.tag.List', no_payload=True)
+    @asyncio.coroutine
+    def vm_tag_list(self):
+        assert not self.arg
+
+        tags = self.dest.tags
+
+        tags = self.fire_event_for_filter(tags)
+
+        return ''.join('{}\n'.format(tag) for tag in sorted(tags))
+
+    @qubes.api.method('admin.vm.tag.Get', no_payload=True)
+    @asyncio.coroutine
+    def vm_tag_get(self):
+        qubes.vm.Tags.validate_tag(self.arg)
+
+        self.fire_event_for_permission()
+
+        return '1' if self.arg in self.dest.tags else '0'
+
+    @qubes.api.method('admin.vm.tag.Set', no_payload=True)
+    @asyncio.coroutine
+    def vm_tag_set(self):
+        qubes.vm.Tags.validate_tag(self.arg)
+
+        self.fire_event_for_permission()
+
+        self.dest.tags.add(self.arg)
+        self.app.save()
+
+    @qubes.api.method('admin.vm.tag.Remove', no_payload=True)
+    @asyncio.coroutine
+    def vm_tag_remove(self):
+        qubes.vm.Tags.validate_tag(self.arg)
+
+        self.fire_event_for_permission()
+
+        try:
+            self.dest.tags.remove(self.arg)
+        except KeyError:
+            raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
+        self.app.save()
+
     @qubes.api.method('admin.pool.List', no_payload=True)
     @asyncio.coroutine
     def pool_list(self):

+ 10 - 1
qubes/exc.py

@@ -101,7 +101,7 @@ class QubesVMNotHaltedError(QubesVMError):
 class QubesNoTemplateError(QubesVMError):
     '''Cannot start domain, because there is no template'''
     def __init__(self, vm, msg=None):
-        super(QubesNoTemplateError, self).__init__(
+        super(QubesNoTemplateError, self).__init__(vm,
             msg or 'Template for the domain {!r} not found'.format(vm.name))
 
 
@@ -162,3 +162,12 @@ class QubesFeatureNotFoundError(QubesException, KeyError):
             'Feature not set for domain {}: {}'.format(domain, feature))
         self.feature = feature
         self.vm = domain
+
+class QubesTagNotFoundError(QubesException, KeyError):
+    '''Tag not set for a given domain'''
+
+    def __init__(self, domain, tag):
+        super().__init__('Tag not set for domain {}: {}'.format(
+            domain, tag))
+        self.vm = domain
+        self.tag = tag

+ 31 - 34
qubes/storage/__init__.py

@@ -431,46 +431,43 @@ class Storage(object):
         os.umask(old_umask)
 
     @asyncio.coroutine
-    def clone(self, src_vm):
-        ''' Clone volumes from the specified vm '''
+    def clone_volume(self, src_vm, name):
+        ''' Clone single volume from the specified vm
+
+        :param QubesVM src_vm: source VM
+        :param str name: name of volume to clone ('root', 'private' etc)
+        :return cloned volume object
+        '''
+        config = self.vm.volume_config[name]
+        dst_pool = self.vm.app.get_pool(config['pool'])
+        dst = dst_pool.init_volume(self.vm, config)
+        src_volume = src_vm.volumes[name]
+        src_pool = src_volume.pool
+        if dst_pool == src_pool:
+            msg = "Cloning volume {!s} from vm {!s}"
+            self.vm.log.info(msg.format(src_volume.name, src_vm.name))
+            clone_op_ret = dst_pool.clone(src_volume, dst)
+        else:
+            msg = "Importing volume {!s} from vm {!s}"
+            self.vm.log.info(msg.format(src_volume.name, src_vm.name))
+            clone_op_ret = dst_pool.import_volume(
+                dst_pool, dst, src_pool, src_volume)
 
         # clone/import functions may be either synchronous or asynchronous
         # in the later case, we need to wait for them to finish
-        clone_op = {}
+        if asyncio.iscoroutine(clone_op_ret):
+            clone_op_ret = yield from clone_op_ret
+        self.vm.volumes[name] = clone_op_ret
+        return self.vm.volumes[name]
+
+    @asyncio.coroutine
+    def clone(self, src_vm):
+        ''' Clone volumes from the specified vm '''
 
         self.vm.volumes = {}
         with VmCreationManager(self.vm):
-            for name, config in self.vm.volume_config.items():
-                dst_pool = self.vm.app.get_pool(config['pool'])
-                dst = dst_pool.init_volume(self.vm, config)
-                src_volume = src_vm.volumes[name]
-                src_pool = src_volume.pool
-                if dst_pool == src_pool:
-                    msg = "Cloning volume {!s} from vm {!s}"
-                    self.vm.log.info(msg.format(src_volume.name, src_vm.name))
-                    clone_op_ret = dst_pool.clone(src_volume, dst)
-                else:
-                    msg = "Importing volume {!s} from vm {!s}"
-                    self.vm.log.info(msg.format(src_volume.name, src_vm.name))
-                    clone_op_ret = dst_pool.import_volume(
-                            dst_pool, dst, src_pool, src_volume)
-                if asyncio.iscoroutine(clone_op_ret):
-                    clone_op[name] = asyncio.ensure_future(clone_op_ret)
-
-            yield from asyncio.wait(x for x in clone_op.values()
-                if inspect.isawaitable(x))
-
-            for name, clone_op_ret in clone_op.items():
-                if inspect.isawaitable(clone_op_ret):
-                    volume = clone_op_ret.result
-                else:
-                    volume = clone_op_ret
-
-                assert volume, "%s.clone() returned '%s'" % (
-                    self.vm.app.get_pool(self.vm.volume_config[name]['pool']).
-                        __class__.__name__, volume)
-
-                self.vm.volumes[name] = volume
+            yield from asyncio.wait(self.clone_volume(src_vm, vol_name)
+                for vol_name in self.vm.volume_config.keys())
 
     @property
     def outdated_volumes(self):

+ 136 - 0
qubes/tests/api_admin.py

@@ -1598,6 +1598,142 @@ class TC_00_VMs(AdminAPITestCase):
                 self.call_mgmt_func(b'admin.vm.volume.Import', b'test-vm1',
                     b'private')
 
+    def test_520_vm_volume_clone(self):
+        self.vm2 = self.app.add_new_vm('AppVM', label='red', name='test-vm2',
+            template='test-template')
+        self.vm.volumes = unittest.mock.MagicMock()
+        self.vm2.volumes = unittest.mock.MagicMock()
+        volumes_conf = {
+            'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
+        }
+        self.vm.volumes.configure_mock(**volumes_conf)
+        self.vm2.volumes.configure_mock(**volumes_conf)
+        self.vm2.storage = unittest.mock.Mock()
+        func_mock = unittest.mock.Mock()
+
+        @asyncio.coroutine
+        def coroutine_mock(*args, **kwargs):
+            return func_mock(*args, **kwargs)
+        self.vm2.storage.clone_volume = coroutine_mock
+
+        self.call_mgmt_func(b'admin.vm.volume.Clone',
+                b'test-vm1', b'private', b'test-vm2')
+        self.assertEqual(self.vm.volumes.mock_calls,
+            [('keys', (), {}),
+             ('__getitem__', ('private', ), {}),
+             ('__getitem__().__hash__', (), {}),
+            ])
+        self.assertEqual(self.vm2.volumes.mock_calls,
+            [unittest.mock.call.keys()])
+        self.assertEqual(self.vm2.storage.mock_calls, [])
+        self.assertEqual(func_mock.mock_calls, [
+            unittest.mock.call(self.vm, 'private')
+        ])
+        self.app.save.assert_called_once_with()
+
+    def test_521_vm_volume_clone_invalid_volume(self):
+        self.vm2 = self.app.add_new_vm('AppVM', label='red', name='test-vm2',
+            template='test-template')
+        self.vm.volumes = unittest.mock.MagicMock()
+        self.vm2.volumes = unittest.mock.MagicMock()
+        volumes_conf = {
+            'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
+        }
+        self.vm.volumes.configure_mock(**volumes_conf)
+        self.vm2.volumes.configure_mock(**volumes_conf)
+        self.vm2.storage = unittest.mock.Mock()
+        func_mock = unittest.mock.Mock()
+
+        @asyncio.coroutine
+        def coroutine_mock(*args, **kwargs):
+            return func_mock(*args, **kwargs)
+        self.vm2.storage.clone_volume = coroutine_mock
+
+        with self.assertRaises(AssertionError):
+            self.call_mgmt_func(b'admin.vm.volume.Clone',
+                    b'test-vm1', b'private123', b'test-vm2')
+        self.assertEqual(self.vm.volumes.mock_calls,
+            [('keys', (), {})])
+        self.assertEqual(self.vm2.volumes.mock_calls, [])
+        self.assertEqual(self.vm2.storage.mock_calls, [])
+        self.assertEqual(func_mock.mock_calls, [])
+        self.assertFalse(self.app.save.called)
+
+    def test_522_vm_volume_clone_invalid_vm(self):
+        self.vm2 = self.app.add_new_vm('AppVM', label='red', name='test-vm2',
+            template='test-template')
+        self.vm.volumes = unittest.mock.MagicMock()
+        self.vm2.volumes = unittest.mock.MagicMock()
+        volumes_conf = {
+            'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
+        }
+        self.vm.volumes.configure_mock(**volumes_conf)
+        self.vm2.volumes.configure_mock(**volumes_conf)
+        self.vm2.storage = unittest.mock.Mock()
+        func_mock = unittest.mock.Mock()
+
+        @asyncio.coroutine
+        def coroutine_mock(*args, **kwargs):
+            return func_mock(*args, **kwargs)
+        self.vm2.storage.clone_volume = coroutine_mock
+
+        with self.assertRaises(AssertionError):
+            self.call_mgmt_func(b'admin.vm.volume.Clone',
+                    b'test-vm1', b'private123', b'no-such-vm')
+        self.assertEqual(self.vm.volumes.mock_calls,
+            [('keys', (), {})])
+        self.assertEqual(self.vm2.volumes.mock_calls, [])
+        self.assertEqual(self.vm2.storage.mock_calls, [])
+        self.assertEqual(func_mock.mock_calls, [])
+        self.assertFalse(self.app.save.called)
+
+    def test_530_tag_list(self):
+        self.vm.tags.add('tag1')
+        self.vm.tags.add('tag2')
+        value = self.call_mgmt_func(b'admin.vm.tag.List', b'test-vm1')
+        self.assertEqual(value, 'tag1\ntag2\n')
+        self.assertFalse(self.app.save.called)
+
+    def test_540_tag_get(self):
+        self.vm.tags.add('tag1')
+        value = self.call_mgmt_func(b'admin.vm.tag.Get', b'test-vm1',
+            b'tag1')
+        self.assertEqual(value, '1')
+        self.assertFalse(self.app.save.called)
+
+    def test_541_tag_get_absent(self):
+        value = self.call_mgmt_func(b'admin.vm.tag.Get', b'test-vm1', b'tag1')
+        self.assertEqual(value, '0')
+        self.assertFalse(self.app.save.called)
+
+    def test_550_tag_remove(self):
+        self.vm.tags.add('tag1')
+        value = self.call_mgmt_func(b'admin.vm.tag.Remove', b'test-vm1',
+            b'tag1')
+        self.assertIsNone(value, None)
+        self.assertNotIn('tag1', self.vm.tags)
+        self.assertTrue(self.app.save.called)
+
+    def test_551_tag_remove_absent(self):
+        with self.assertRaises(qubes.exc.QubesTagNotFoundError):
+            self.call_mgmt_func(b'admin.vm.tag.Remove',
+                b'test-vm1', b'tag1')
+        self.assertFalse(self.app.save.called)
+
+    def test_560_tag_set(self):
+        value = self.call_mgmt_func(b'admin.vm.tag.Set',
+            b'test-vm1', b'tag1')
+        self.assertIsNone(value)
+        self.assertIn('tag1', self.vm.tags)
+        self.assertTrue(self.app.save.called)
+
+    def test_561_tag_set_invalid(self):
+        with self.assertRaises(AssertionError):
+            self.call_mgmt_func(b'admin.vm.tag.Set',
+                b'test-vm1', b'+.some-tag')
+        self.assertNotIn('+.some-tag', self.vm.tags)
+        self.assertFalse(self.app.save.called)
+
     def test_990_vm_unexpected_payload(self):
         methods_with_no_payload = [
             b'admin.vm.List',

+ 5 - 0
qubes/vm/__init__.py

@@ -244,6 +244,11 @@ class Tags(set):
     # end of overriding
     #
 
+    @staticmethod
+    def validate_tag(tag):
+        safe_set = string.ascii_letters + string.digits + '-_'
+        assert all((x in safe_set) for x in tag)
+
 
 class BaseVM(qubes.PropertyHolder):
     '''Base class for all VMs