tools/qvm-volume: add 'import' action

Add support for importing volume data with qvm-volume tool.
This could be also used to clear volume by issuing:

    qvm-volume import --no-resize some-vm:private /dev/null

QubesOS/qubes-issues#5192
This commit is contained in:
Marek Marczykowski-Górecki 2019-07-29 20:53:45 +02:00
parent fdc632c959
commit e700af9eb2
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
3 changed files with 235 additions and 0 deletions

View File

@ -110,6 +110,32 @@ Revert a volume to previous revision.
aliases: rv, r aliases: rv, r
import
^^^^^^
| :command:`qvm-volume import` [-h] [--size=SIZE|--no-resize] [--verbose] [--quiet] *VMNAME:VOLUME* *PATH*
Import file *PATH* into volume *VMNAME:VOLUME*. Use `-` as *PATH* to import from
stdin.
The tool will try to resize volume to match input size before the import. In
case of importing from stdin, you may need to provide size explicitly with
`--size` option. You can keep previous volume size by using `--no-resize`
option.
A specific use case is importing empty data to clear private volume:
| :command:`qvm-volume` import --no-resize some-vm:private /dev/null
Old data will be stored as a revision, subject to `revisions_to_keep` limit.
.. option:: --size
Provide the size explicitly, instead of using *FILE* size.
.. option:: --no-resize
Do not resize volume before the import.
Authors Authors
------- -------

View File

@ -17,6 +17,8 @@
# #
# You should have received a copy of the GNU Lesser General Public License along # You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>. # with this program; if not, see <http://www.gnu.org/licenses/>.
import tempfile
import unittest.mock
import qubesadmin.tests import qubesadmin.tests
import qubesadmin.tests.tools import qubesadmin.tests.tools
@ -487,3 +489,163 @@ class TC_00_qvm_volume(qubesadmin.tests.QubesTestCase):
'200201010000\n' '200201010000\n'
'200301010000\n') '200301010000\n')
self.assertAllCalled() self.assertAllCalled()
def test_050_import_file(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'save_on_stop=True\n' \
b'snap_on_start=False\n' \
b'revisions_to_keep=0\n' \
b'is_outdated=False\n'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Resize', 'private', b'9')] = \
b'0\x00'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Import', 'private', b'test-data')] = \
b'0\x00'
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(b'test-data')
input_file.seek(0)
input_file.flush()
self.assertEqual(0,
qubesadmin.tools.qvm_volume.main(
['import', 'testvm:private', input_file.name],
app=self.app))
self.assertAllCalled()
def test_051_import_stdin(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'save_on_stop=True\n' \
b'snap_on_start=False\n' \
b'revisions_to_keep=0\n' \
b'is_outdated=False\n'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Resize', 'private', b'9')] = \
b'0\x00'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Import', 'private', b'test-data')] = \
b'0\x00'
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(b'test-data')
input_file.seek(0)
with unittest.mock.patch('sys.stdin') as mock_stdin:
mock_stdin.buffer = input_file
self.assertEqual(0,
qubesadmin.tools.qvm_volume.main(
['import', 'testvm:private', '-'],
app=self.app))
self.assertAllCalled()
def test_052_import_file_size(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'save_on_stop=True\n' \
b'snap_on_start=False\n' \
b'revisions_to_keep=0\n' \
b'is_outdated=False\n'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Resize', 'private', b'512')] = \
b'0\x00'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Import', 'private', b'test-data')] = \
b'0\x00'
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(b'test-data')
input_file.seek(0)
input_file.flush()
self.assertEqual(0,
qubesadmin.tools.qvm_volume.main(
['import', '--size=512', 'testvm:private', input_file.name],
app=self.app))
self.assertAllCalled()
def test_053_import_file_noresize(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'save_on_stop=True\n' \
b'snap_on_start=False\n' \
b'revisions_to_keep=0\n' \
b'is_outdated=False\n'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Import', 'private', b'test-data')] = \
b'0\x00'
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(b'test-data')
input_file.seek(0)
input_file.flush()
self.assertEqual(0,
qubesadmin.tools.qvm_volume.main(
['import', '--no-resize', 'testvm:private', input_file.name],
app=self.app))
self.assertAllCalled()
def test_053_import_file_matching_size(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=9\n' \
b'usage=1\n' \
b'rw=True\n' \
b'save_on_stop=True\n' \
b'snap_on_start=False\n' \
b'revisions_to_keep=0\n' \
b'is_outdated=False\n'
self.app.expected_calls[
('testvm', 'admin.vm.volume.Import', 'private', b'test-data')] = \
b'0\x00'
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(b'test-data')
input_file.seek(0)
input_file.flush()
self.assertEqual(0,
qubesadmin.tools.qvm_volume.main(
['import', 'testvm:private', input_file.name],
app=self.app))
self.assertAllCalled()

View File

@ -25,6 +25,7 @@
from __future__ import print_function from __future__ import print_function
import argparse import argparse
import os
import sys import sys
import collections import collections
@ -132,6 +133,37 @@ def config_volume(args):
'Invalid property: {}'.format(args.property)) 'Invalid property: {}'.format(args.property))
setattr(volume, args.property, args.value) setattr(volume, args.property, args.value)
def import_volume(args):
''' Import a file into volume '''
volume = args.volume
old_size = volume.size
input_path = args.input_path
if input_path == '-':
input_file = sys.stdin.buffer
else:
input_file = open(input_path, 'rb')
try:
if not args.no_resize:
if args.size:
size = args.size
else:
try:
size = os.stat(input_file.fileno()).st_size
except OSError as e:
raise qubesadmin.exc.QubesException(
'Failed to get %s file size, '
'specify it explicitly with --size, '
'or use --no-resize option', str(e))
if size > old_size:
volume.resize(size)
volume.import_data(stream=input_file)
if not args.no_resize and size < old_size:
volume.resize(size)
finally:
if input_path != '-':
input_file.close()
def list_volumes(args): def list_volumes(args):
''' Called by the parser to execute the qvm-volume list subcommand. ''' ''' Called by the parser to execute the qvm-volume list subcommand. '''
app = args.app app = args.app
@ -276,6 +308,20 @@ def init_config_parser(sub_parsers):
info_parser.add_argument(dest='value', action='store') info_parser.add_argument(dest='value', action='store')
info_parser.set_defaults(func=config_volume) info_parser.set_defaults(func=config_volume)
def init_import_parser(sub_parsers):
''' Add 'import' action related options '''
import_parser = sub_parsers.add_parser(
'import', help='import volume data')
import_parser.add_argument(metavar='VM:VOLUME', dest='volume',
action=qubesadmin.tools.VMVolumeAction)
import_parser.add_argument('input_path', metavar='PATH',
help='File path to import, use \'-\' for standard input')
import_parser.add_argument('--size', action='store', type=int,
help='Set volume size to this value in bytes')
import_parser.add_argument('--no-resize', action='store_true',
help='Do not resize volume before importing data')
import_parser.set_defaults(func=import_volume)
def get_parser(): def get_parser():
'''Create :py:class:`argparse.ArgumentParser` suitable for '''Create :py:class:`argparse.ArgumentParser` suitable for
:program:`qvm-volume`. :program:`qvm-volume`.
@ -293,6 +339,7 @@ def get_parser():
init_extend_parser(sub_parsers) init_extend_parser(sub_parsers)
init_list_parser(sub_parsers) init_list_parser(sub_parsers)
init_revert_parser(sub_parsers) init_revert_parser(sub_parsers)
init_import_parser(sub_parsers)
# default action # default action
parser.set_defaults(func=list_volumes) parser.set_defaults(func=list_volumes)