Bläddra i källkod

qvm-volume: refuse to shrink volume unless --force option is used

Right now Admin API backend will refuse to shrink volume anyway, but
we're planning to relax this restriction. Make sure the client side
(qvm-volume tool here, GUI VM settings already have this in place) will
employ appropriate safety check.

QubesOS/qubes-issues#3725
Marek Marczykowski-Górecki 6 år sedan
förälder
incheckning
70b15c2eae
3 ändrade filer med 94 tillägg och 8 borttagningar
  1. 14 3
      doc/manpages/qvm-volume.rst
  2. 69 4
      qubesadmin/tests/tools/qvm_volume.py
  3. 11 1
      qubesadmin/tools/qvm_volume.py

+ 14 - 3
doc/manpages/qvm-volume.rst

@@ -84,11 +84,22 @@ Set property of given volume. Properties currently possible to change:
 
 aliases: c, set, s
 
-extend
+resize
 ^^^^^^
-| :command:`qvm-volume extend` [-h] [--verbose] [--quiet] *VMNAME:VOLUME* *NEW_SIZE*
+| :command:`qvm-volume resize` [-h] [--force|-f] [--verbose] [--quiet] *VMNAME:VOLUME* *NEW_SIZE*
 
-Extend the volume with *POOL_NAME:VOLUME_ID* TO *NEW_SIZE*
+Resize the volume with *VMNAME:VOLUME* TO *NEW_SIZE*
+
+If new size is smaller than current, the tool will refuse to continue unless
+`--force` option is used. One should be very careful about that, because
+shrinking volume without shrinking filesystem and other data inside first, will
+surely end with data loss.
+
+.. option:: -f, --force
+
+   Force operation even if new size is smaller than the current one.
+
+aliases: extend
 
 revert
 ^^^^^^

+ 69 - 4
qubesadmin/tests/tools/qvm_volume.py

@@ -153,6 +153,18 @@ class TC_00_qvm_volume(qubesadmin.tests.QubesTestCase):
         self.app.expected_calls[
             ('testvm', 'admin.vm.volume.List', None, None)] = \
             b'0\x00root\nprivate\n'
+        self.app.expected_calls[
+            ('testvm', 'admin.vm.volume.Info', 'private', None)] = \
+            b'0\x00pool=lvm\n' \
+            b'vid=qubes_dom0/vm-testvm-private\n' \
+            b'size=2147483648\n' \
+            b'usage=10000000\n' \
+            b'rw=True\n' \
+            b'source=\n' \
+            b'save_on_stop=True\n' \
+            b'snap_on_start=False\n' \
+            b'revisions_to_keep=3\n' \
+            b'is_outdated=False\n'
         self.app.expected_calls[
             ('testvm', 'admin.vm.volume.Resize', 'private', b'10737418240')] = \
             b'0\x00'
@@ -169,15 +181,68 @@ class TC_00_qvm_volume(qubesadmin.tests.QubesTestCase):
             ('testvm', 'admin.vm.volume.List', None, None)] = \
             b'0\x00root\nprivate\n'
         self.app.expected_calls[
-            ('testvm', 'admin.vm.volume.Resize', 'private', b'1073741824')] = \
+            ('testvm', 'admin.vm.volume.Info', 'private', None)] = \
+            b'0\x00pool=lvm\n' \
+            b'vid=qubes_dom0/vm-testvm-private\n' \
+            b'size=2147483648\n' \
+            b'usage=10000000\n' \
+            b'rw=True\n' \
+            b'source=\n' \
+            b'save_on_stop=True\n' \
+            b'snap_on_start=False\n' \
+            b'revisions_to_keep=3\n' \
+            b'is_outdated=False\n'
+        self.app.expected_calls[
+            ('testvm', 'admin.vm.volume.Resize', 'private', b'10737418240')] = \
             b'2\x00StoragePoolException\x00\x00Failed to resize volume: ' \
-            b'shrink not allowed\x00'
+            b'error: success\x00'
+        with qubesadmin.tests.tools.StderrBuffer() as stderr:
+            self.assertEqual(1,
+                qubesadmin.tools.qvm_volume.main(
+                    ['extend', 'testvm:private', '10GiB'],
+                    app=self.app))
+        self.assertIn('error: success', stderr.getvalue())
+        self.assertAllCalled()
+
+    def test_012_extend_deny_shrink(self):
+        self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00testvm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('testvm', 'admin.vm.volume.List', None, None)] = \
+            b'0\x00root\nprivate\n'
+        self.app.expected_calls[
+            ('testvm', 'admin.vm.volume.Info', 'private', None)] = \
+            b'0\x00pool=lvm\n' \
+            b'vid=qubes_dom0/vm-testvm-private\n' \
+            b'size=2147483648\n' \
+            b'usage=10000000\n' \
+            b'rw=True\n' \
+            b'source=\n' \
+            b'save_on_stop=True\n' \
+            b'snap_on_start=False\n' \
+            b'revisions_to_keep=3\n' \
+            b'is_outdated=False\n'
         with qubesadmin.tests.tools.StderrBuffer() as stderr:
             self.assertEqual(1,
                 qubesadmin.tools.qvm_volume.main(
-                    ['extend', 'testvm:private', '1GiB'],
+                    ['resize', 'testvm:private', '1GiB'],
                     app=self.app))
-        self.assertIn('shrink not allowed', stderr.getvalue())
+        self.assertIn('shrinking of private is disabled', stderr.getvalue())
+        self.assertAllCalled()
+
+    def test_013_resize_force_shrink(self):
+        self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00testvm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('testvm', 'admin.vm.volume.List', None, None)] = \
+            b'0\x00root\nprivate\n'
+        self.app.expected_calls[
+            ('testvm', 'admin.vm.volume.Resize', 'private', b'1073741824')] = \
+            b'0\x00'
+        self.assertEqual(0,
+            qubesadmin.tools.qvm_volume.main(
+                ['resize', '-f', 'testvm:private', '1GiB'],
+                app=self.app))
         self.assertAllCalled()
 
     def test_020_revert(self):

+ 11 - 1
qubesadmin/tools/qvm_volume.py

@@ -199,6 +199,13 @@ def extend_volumes(args):
     '''
     volume = args.volume
     size = qubesadmin.utils.parse_size(args.size)
+    if not args.force and size < volume.size:
+        raise qubesadmin.exc.StoragePoolException(
+            'For your own safety, shrinking of %s is'
+            ' disabled (%d < %d). If you really know what you'
+            ' are doing, resize filesystem manually first, then use `-f` '
+            'option.' %
+            (volume.name, size, volume.size))
     volume.resize(size)
 
 
@@ -237,10 +244,13 @@ def init_revert_parser(sub_parsers):
 def init_extend_parser(sub_parsers):
     ''' Add 'extend' action related options '''
     extend_parser = sub_parsers.add_parser(
-        "extend", help="extend volume from domain")
+        "resize", aliases=('extend', ), help="resize volume for domain")
     extend_parser.add_argument(metavar='VM:VOLUME', dest='volume',
                                action=qubesadmin.tools.VMVolumeAction)
     extend_parser.add_argument('size', help='New size in bytes')
+    extend_parser.add_argument('--force', '-f', action='store_true',
+        help='Force operation, even if new size is smaller than the current '
+             'one')
     extend_parser.set_defaults(func=extend_volumes)
 
 def init_info_parser(sub_parsers):