diff --git a/doc/manpages/qvm-volume.rst b/doc/manpages/qvm-volume.rst index 2f17b47..27f1179 100644 --- a/doc/manpages/qvm-volume.rst +++ b/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 ^^^^^^ diff --git a/qubesadmin/tests/tools/qvm_volume.py b/qubesadmin/tests/tools/qvm_volume.py index dc46fbb..7f27b9c 100644 --- a/qubesadmin/tests/tools/qvm_volume.py +++ b/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', '1GiB'], + ['extend', 'testvm:private', '10GiB'], app=self.app)) - self.assertIn('shrink not allowed', stderr.getvalue()) + 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( + ['resize', 'testvm:private', '1GiB'], + app=self.app)) + 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): diff --git a/qubesadmin/tools/qvm_volume.py b/qubesadmin/tools/qvm_volume.py index 0616470..62579d0 100644 --- a/qubesadmin/tools/qvm_volume.py +++ b/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):