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:
parent
fdc632c959
commit
e700af9eb2
@ -110,6 +110,32 @@ Revert a volume to previous revision.
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
|
@ -17,6 +17,8 @@
|
||||
#
|
||||
# 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/>.
|
||||
import tempfile
|
||||
import unittest.mock
|
||||
|
||||
import qubesadmin.tests
|
||||
import qubesadmin.tests.tools
|
||||
@ -487,3 +489,163 @@ class TC_00_qvm_volume(qubesadmin.tests.QubesTestCase):
|
||||
'200201010000\n'
|
||||
'200301010000\n')
|
||||
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()
|
||||
|
@ -25,6 +25,7 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import collections
|
||||
@ -132,6 +133,37 @@ def config_volume(args):
|
||||
'Invalid property: {}'.format(args.property))
|
||||
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):
|
||||
''' Called by the parser to execute the qvm-volume list subcommand. '''
|
||||
app = args.app
|
||||
@ -276,6 +308,20 @@ def init_config_parser(sub_parsers):
|
||||
info_parser.add_argument(dest='value', action='store')
|
||||
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():
|
||||
'''Create :py:class:`argparse.ArgumentParser` suitable for
|
||||
:program:`qvm-volume`.
|
||||
@ -293,6 +339,7 @@ def get_parser():
|
||||
init_extend_parser(sub_parsers)
|
||||
init_list_parser(sub_parsers)
|
||||
init_revert_parser(sub_parsers)
|
||||
init_import_parser(sub_parsers)
|
||||
# default action
|
||||
parser.set_defaults(func=list_volumes)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user