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
|
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
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user