Add qvm-pool and qvm-volume tool
This commit is contained in:
parent
1fd09f4e58
commit
36d8ee9b32
94
qubesmgmt/tests/tools/qvm_pool.py
Normal file
94
qubesmgmt/tests/tools/qvm_pool.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# -*- encoding: utf8 -*-
|
||||||
|
#
|
||||||
|
# The Qubes OS Project, http://www.qubes-os.org
|
||||||
|
#
|
||||||
|
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||||
|
# <marmarek@invisiblethingslab.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 qubesmgmt.tests
|
||||||
|
import qubesmgmt.tests.tools
|
||||||
|
import qubesmgmt.tools.qvm_pool
|
||||||
|
|
||||||
|
|
||||||
|
class TC_00_qvm_pool(qubesmgmt.tests.QubesTestCase):
|
||||||
|
def test_000_list(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00pool-file\npool-lvm\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Info', 'pool-file', None)] = \
|
||||||
|
b'0\x00driver=file\ndir_path=/var/lib/qubes\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Info', 'pool-lvm', None)] = \
|
||||||
|
b'0\x00driver=lvm\nvolume_group=qubes_dom0\nthin_pool=pool00\n'
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_pool.main(['-l'], app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'NAME DRIVER\n'
|
||||||
|
'pool-file file\n'
|
||||||
|
'pool-lvm lvm\n')
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_010_list_drivers(self):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.ListDrivers', None, None)] = \
|
||||||
|
b'0\x00file dir_path revisions_to_keep\n' \
|
||||||
|
b'lvm volume_group thin_pool revisions_to_keep\n'
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_pool.main(['--help-drivers'], app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'DRIVER OPTIONS\n'
|
||||||
|
'file dir_path, revisions_to_keep\n'
|
||||||
|
'lvm volume_group, thin_pool, revisions_to_keep\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_020_add(self):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Add', 'file',
|
||||||
|
b'name=test-pool\ndir_path=/some/path\n')] = b'0\x00'
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_pool.main(
|
||||||
|
['--add', 'test-pool', 'file', '-o', 'dir_path=/some/path'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_030_remove(self):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Remove', 'test-pool', None)] = b'0\x00'
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_pool.main(['--remove', 'test-pool'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_040_info(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00pool-file\npool-lvm\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.pool.Info', 'pool-lvm', None)] = \
|
||||||
|
b'0\x00driver=lvm\nvolume_group=qubes_dom0\nthin_pool=pool00\n'
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_pool.main(['-i', 'pool-lvm'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'name pool-lvm\n'
|
||||||
|
'driver lvm\n'
|
||||||
|
'thin_pool pool00\n'
|
||||||
|
'volume_group qubes_dom0\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
167
qubesmgmt/tests/tools/qvm_volume.py
Normal file
167
qubesmgmt/tests/tools/qvm_volume.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# -*- encoding: utf8 -*-
|
||||||
|
#
|
||||||
|
# The Qubes OS Project, http://www.qubes-os.org
|
||||||
|
#
|
||||||
|
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||||
|
# <marmarek@invisiblethingslab.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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 qubesmgmt.tests
|
||||||
|
import qubesmgmt.tests.tools
|
||||||
|
import qubesmgmt.tools.qvm_volume
|
||||||
|
|
||||||
|
|
||||||
|
class TC_00_qvm_volume(qubesmgmt.tests.QubesTestCase):
|
||||||
|
|
||||||
|
def setup_expected_calls_for_list(self, vms=('vm1', 'sys-net')):
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.vm.List', None, None)] = \
|
||||||
|
b'0\x00vm1 class=AppVM state=Running\n' \
|
||||||
|
b'sys-net class=AppVM state=Running\n'
|
||||||
|
for vm in vms:
|
||||||
|
for vol in ('root', 'private'):
|
||||||
|
self.app.expected_calls[
|
||||||
|
(vm, 'mgmt.vm.volume.Info', vol, None)] = \
|
||||||
|
b'0\x00' + \
|
||||||
|
(b'pool=pool-file\n' if vol == 'root' else
|
||||||
|
b'pool=other-pool\n') + \
|
||||||
|
b'vid=' + vm.encode() + b'-' + vol.encode() + b'\n' \
|
||||||
|
b'internal=True\n' \
|
||||||
|
b'size=10737418240\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
(vm, 'mgmt.vm.volume.ListSnapshots', vol, None)] = \
|
||||||
|
b'0\x00snap1\n' if vol == 'private' else b'0\x00'
|
||||||
|
self.app.expected_calls[
|
||||||
|
(vm, 'mgmt.vm.volume.List', None, None)] = \
|
||||||
|
b'0\x00root\nprivate\n'
|
||||||
|
|
||||||
|
def test_000_list(self):
|
||||||
|
self.setup_expected_calls_for_list()
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(['ls', '-i'], app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'POOL:VOLUME VMNAME VOLUME_NAME '
|
||||||
|
'REVERT_POSSIBLE\n'
|
||||||
|
'other-pool:sys-net-private sys-net private Yes\n'
|
||||||
|
'other-pool:vm1-private vm1 private Yes\n'
|
||||||
|
'pool-file:sys-net-root sys-net root No\n'
|
||||||
|
'pool-file:vm1-root vm1 root No\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_001_list_domain(self):
|
||||||
|
self.setup_expected_calls_for_list(vms=('vm1',))
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(['ls', '-i', 'vm1'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
|
||||||
|
'other-pool:vm1-private vm1 private Yes\n'
|
||||||
|
'pool-file:vm1-root vm1 root No\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_002_list_domain_pool(self):
|
||||||
|
self.setup_expected_calls_for_list(vms=('vm1',))
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00pool-file\nother-pool\n'
|
||||||
|
del self.app.expected_calls[
|
||||||
|
('vm1', 'mgmt.vm.volume.ListSnapshots', 'private', None)]
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(
|
||||||
|
['ls', '-i', '-p', 'pool-file', 'vm1'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
|
||||||
|
'pool-file:vm1-root vm1 root No\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_003_list_pool(self):
|
||||||
|
self.setup_expected_calls_for_list()
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.pool.List', None, None)] = \
|
||||||
|
b'0\x00pool-file\nother-pool\n'
|
||||||
|
del self.app.expected_calls[
|
||||||
|
('vm1', 'mgmt.vm.volume.ListSnapshots', 'private', None)]
|
||||||
|
del self.app.expected_calls[
|
||||||
|
('sys-net', 'mgmt.vm.volume.ListSnapshots', 'private', None)]
|
||||||
|
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(
|
||||||
|
['ls', '-i', '-p', 'pool-file'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
|
||||||
|
'pool-file:sys-net-root sys-net root No\n'
|
||||||
|
'pool-file:vm1-root vm1 root No\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_004_list_multiple_domains(self):
|
||||||
|
self.setup_expected_calls_for_list(vms=('vm1', 'vm2'))
|
||||||
|
self.app.expected_calls[
|
||||||
|
('dom0', 'mgmt.vm.List', None, None)] = \
|
||||||
|
b'0\x00vm1 class=AppVM state=Running\n' \
|
||||||
|
b'vm2 class=AppVM state=Running\n' \
|
||||||
|
b'vm3 class=AppVM state=Running\n'
|
||||||
|
with qubesmgmt.tests.tools.StdoutBuffer() as stdout:
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(
|
||||||
|
['ls', '-i', 'vm1', 'vm2'], app=self.app))
|
||||||
|
self.assertEqual(stdout.getvalue(),
|
||||||
|
'POOL:VOLUME VMNAME VOLUME_NAME REVERT_POSSIBLE\n'
|
||||||
|
'other-pool:vm1-private vm1 private Yes\n'
|
||||||
|
'other-pool:vm2-private vm2 private Yes\n'
|
||||||
|
'pool-file:vm1-root vm1 root No\n'
|
||||||
|
'pool-file:vm2-root vm2 root No\n'
|
||||||
|
)
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_010_extend(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.vm.List', None, None)] = \
|
||||||
|
b'0\x00testvm class=AppVM state=Running\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('testvm', 'mgmt.vm.volume.List', None, None)] = \
|
||||||
|
b'0\x00root\nprivate\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('testvm', 'mgmt.vm.volume.Resize', 'private', b'10737418240')] = \
|
||||||
|
b'0\x00'
|
||||||
|
self.assertEqual(0,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(
|
||||||
|
['extend', 'testvm:private', '10GiB'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertAllCalled()
|
||||||
|
|
||||||
|
def test_011_extend_error(self):
|
||||||
|
self.app.expected_calls[('dom0', 'mgmt.vm.List', None, None)] = \
|
||||||
|
b'0\x00testvm class=AppVM state=Running\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('testvm', 'mgmt.vm.volume.List', None, None)] = \
|
||||||
|
b'0\x00root\nprivate\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('testvm', 'mgmt.vm.volume.Resize', 'private', b'1073741824')] = \
|
||||||
|
b'2\x00StoragePoolException\x00\x00Failed to resize volume: ' \
|
||||||
|
b'shrink not allowed\x00'
|
||||||
|
with qubesmgmt.tests.tools.StderrBuffer() as stderr:
|
||||||
|
self.assertEqual(1,
|
||||||
|
qubesmgmt.tools.qvm_volume.main(
|
||||||
|
['extend', 'testvm:private', '1GiB'],
|
||||||
|
app=self.app))
|
||||||
|
self.assertIn('shrink not allowed', stderr.getvalue())
|
||||||
|
self.assertAllCalled()
|
@ -229,6 +229,45 @@ class VolumeAction(QubesAction):
|
|||||||
parser.error('expected a pool & volume id combination like foo:bar')
|
parser.error('expected a pool & volume id combination like foo:bar')
|
||||||
|
|
||||||
|
|
||||||
|
class VMVolumeAction(QubesAction):
|
||||||
|
''' Action for argument parser that gets the
|
||||||
|
:py:class:``qubes.storage.Volume`` from a VM:VOLUME string.
|
||||||
|
'''
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
def __init__(self, help='A pool & volume id combination',
|
||||||
|
required=True, **kwargs):
|
||||||
|
# pylint: disable=redefined-builtin
|
||||||
|
super(VMVolumeAction, self).__init__(help=help, required=required,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
''' Set ``namespace.vmname`` to ``values`` '''
|
||||||
|
setattr(namespace, self.dest, values)
|
||||||
|
|
||||||
|
def parse_qubes_app(self, parser, namespace):
|
||||||
|
''' Acquire the :py:class:``qubes.storage.Volume`` object from
|
||||||
|
``namespace.app``.
|
||||||
|
'''
|
||||||
|
assert hasattr(namespace, 'app')
|
||||||
|
app = namespace.app
|
||||||
|
|
||||||
|
try:
|
||||||
|
vm_name, vol_name = getattr(namespace, self.dest).split(':')
|
||||||
|
try:
|
||||||
|
vm = app.domains[vm_name]
|
||||||
|
try:
|
||||||
|
volume = vm.volumes[vol_name]
|
||||||
|
setattr(namespace, self.dest, volume)
|
||||||
|
except KeyError:
|
||||||
|
parser.error_runtime('vm {!r} has no volume {!r}'.format(
|
||||||
|
vm_name, vol_name))
|
||||||
|
except KeyError:
|
||||||
|
parser.error_runtime('no vm {!r}'.format(vm_name))
|
||||||
|
except ValueError:
|
||||||
|
parser.error('expected a vm & volume combination like foo:bar')
|
||||||
|
|
||||||
|
|
||||||
class PoolsAction(QubesAction):
|
class PoolsAction(QubesAction):
|
||||||
''' Action for argument parser to gather multiple pools '''
|
''' Action for argument parser to gather multiple pools '''
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
@ -247,7 +286,7 @@ class PoolsAction(QubesAction):
|
|||||||
pool_names = getattr(namespace, self.dest)
|
pool_names = getattr(namespace, self.dest)
|
||||||
if pool_names:
|
if pool_names:
|
||||||
try:
|
try:
|
||||||
pools = [app.get_pool(name) for name in pool_names]
|
pools = [app.pools[name] for name in pool_names]
|
||||||
setattr(namespace, self.dest, pools)
|
setattr(namespace, self.dest, pools)
|
||||||
except qubesmgmt.exc.QubesException as e:
|
except qubesmgmt.exc.QubesException as e:
|
||||||
parser.error(str(e))
|
parser.error(str(e))
|
||||||
|
178
qubesmgmt/tools/qvm_pool.py
Normal file
178
qubesmgmt/tools/qvm_pool.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
#
|
||||||
|
# The Qubes OS Project, http://www.qubes-os.org
|
||||||
|
#
|
||||||
|
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
'''Manages Qubes pools and their options'''
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import qubesmgmt
|
||||||
|
import qubesmgmt.exc
|
||||||
|
import qubesmgmt.storage
|
||||||
|
import qubesmgmt.tools
|
||||||
|
|
||||||
|
|
||||||
|
class _Info(qubesmgmt.tools.PoolsAction):
|
||||||
|
''' Action for argument parser that displays pool info and exits. '''
|
||||||
|
|
||||||
|
def __init__(self, option_strings, help='print pool info and exit',
|
||||||
|
**kwargs):
|
||||||
|
# pylint: disable=redefined-builtin
|
||||||
|
super(_Info, self).__init__(option_strings, help=help, **kwargs)
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
setattr(namespace, 'command', 'info')
|
||||||
|
super(_Info, self).__call__(parser, namespace, values, option_string)
|
||||||
|
|
||||||
|
|
||||||
|
def pool_info(pool):
|
||||||
|
''' Prints out pool name and config '''
|
||||||
|
data = [("name", pool.name)]
|
||||||
|
data += [i for i in sorted(pool.config.items()) if i[0] != 'name']
|
||||||
|
qubesmgmt.tools.print_table(data)
|
||||||
|
|
||||||
|
|
||||||
|
def list_pools(app):
|
||||||
|
''' Prints out all known pools and their drivers '''
|
||||||
|
result = [('NAME', 'DRIVER')]
|
||||||
|
for pool in app.pools:
|
||||||
|
result += [(pool.name, pool.driver)]
|
||||||
|
qubesmgmt.tools.print_table(result)
|
||||||
|
|
||||||
|
|
||||||
|
class _Remove(argparse.Action):
|
||||||
|
''' Action for argument parser that removes a pool '''
|
||||||
|
|
||||||
|
def __init__(self, option_strings, dest=None, default=None, metavar=None):
|
||||||
|
super(_Remove, self).__init__(option_strings=option_strings,
|
||||||
|
dest=dest,
|
||||||
|
metavar=metavar,
|
||||||
|
default=default,
|
||||||
|
help='remove pool')
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, name, option_string=None):
|
||||||
|
setattr(namespace, 'command', 'remove')
|
||||||
|
setattr(namespace, 'name', name)
|
||||||
|
|
||||||
|
|
||||||
|
class _Add(argparse.Action):
|
||||||
|
''' Action for argument parser that adds a pool. '''
|
||||||
|
|
||||||
|
def __init__(self, option_strings, dest=None, default=None, metavar=None):
|
||||||
|
super(_Add, self).__init__(option_strings=option_strings,
|
||||||
|
dest=dest,
|
||||||
|
metavar=metavar,
|
||||||
|
default=default,
|
||||||
|
nargs=2,
|
||||||
|
help='add pool')
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
name, driver = values
|
||||||
|
setattr(namespace, 'command', 'add')
|
||||||
|
setattr(namespace, 'name', name)
|
||||||
|
setattr(namespace, 'driver', driver)
|
||||||
|
|
||||||
|
|
||||||
|
class _Options(argparse.Action):
|
||||||
|
''' Action for argument parser that parsers options. '''
|
||||||
|
|
||||||
|
def __init__(self, option_strings, dest, default, metavar='options'):
|
||||||
|
super(_Options, self).__init__(
|
||||||
|
option_strings=option_strings,
|
||||||
|
dest=dest,
|
||||||
|
metavar=metavar,
|
||||||
|
default=default,
|
||||||
|
help='comma-separated list of driver options')
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, options, option_string=None):
|
||||||
|
setattr(namespace, 'options',
|
||||||
|
dict([option.split('=', 1) for option in options.split(',')]))
|
||||||
|
|
||||||
|
|
||||||
|
def get_parser():
|
||||||
|
''' Parses the provided args '''
|
||||||
|
parser = qubesmgmt.tools.QubesArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('-o', action=_Options, dest='options', default={})
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument('-l',
|
||||||
|
'--list',
|
||||||
|
dest='command',
|
||||||
|
const='list',
|
||||||
|
action='store_const',
|
||||||
|
help='list all pools and exit (default action)')
|
||||||
|
group.add_argument('-i', '--info', metavar='POOLNAME', dest='pools',
|
||||||
|
action=_Info, default=[])
|
||||||
|
group.add_argument('-a',
|
||||||
|
'--add',
|
||||||
|
action=_Add,
|
||||||
|
dest='command',
|
||||||
|
metavar=('NAME', 'DRIVER'))
|
||||||
|
group.add_argument('-r', '--remove', metavar='NAME', action=_Remove)
|
||||||
|
group.add_argument('--help-drivers',
|
||||||
|
dest='command',
|
||||||
|
const='list-drivers',
|
||||||
|
action='store_const',
|
||||||
|
help='list all drivers with their options and exit')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None, app=None):
|
||||||
|
'''Main routine of :program:`qvm-pools`.
|
||||||
|
|
||||||
|
:param list args: Optional arguments to override those delivered from \
|
||||||
|
command line.
|
||||||
|
'''
|
||||||
|
parser = get_parser()
|
||||||
|
try:
|
||||||
|
args = parser.parse_args(args, app=app)
|
||||||
|
except qubesmgmt.exc.QubesException as e:
|
||||||
|
parser.print_error(str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.command is None or args.command == 'list':
|
||||||
|
list_pools(args.app)
|
||||||
|
elif args.command == 'list-drivers':
|
||||||
|
result = [('DRIVER', 'OPTIONS')]
|
||||||
|
for driver in sorted(args.app.pool_drivers):
|
||||||
|
params = args.app.pool_driver_parameters(driver)
|
||||||
|
driver_options = ', '.join(params)
|
||||||
|
result += [(driver, driver_options)]
|
||||||
|
qubesmgmt.tools.print_table(result)
|
||||||
|
elif args.command == 'add':
|
||||||
|
try:
|
||||||
|
args.app.add_pool(name=args.name, driver=args.driver,
|
||||||
|
**args.options)
|
||||||
|
except qubesmgmt.exc.QubesException as e:
|
||||||
|
parser.error('failed to add pool %s: %s\n' % (args.name, str(e)))
|
||||||
|
elif args.command == 'remove':
|
||||||
|
try:
|
||||||
|
args.app.remove_pool(args.name)
|
||||||
|
except KeyError:
|
||||||
|
parser.print_error('no such pool %s\n' % args.name)
|
||||||
|
elif args.command == 'info':
|
||||||
|
for pool in args.pools:
|
||||||
|
pool_info(pool)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
227
qubesmgmt/tools/qvm_volume.py
Normal file
227
qubesmgmt/tools/qvm_volume.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# encoding=utf-8
|
||||||
|
#
|
||||||
|
# The Qubes OS Project, http://www.qubes-os.org
|
||||||
|
#
|
||||||
|
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||||
|
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||||
|
# <marmarek@invisiblethingslab.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
'''Qubes volume management'''
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import qubesmgmt
|
||||||
|
import qubesmgmt.exc
|
||||||
|
import qubesmgmt.tools
|
||||||
|
import qubesmgmt.utils
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_table(vd_list, full=False):
|
||||||
|
''' Converts a list of :py:class:`VolumeData` objects to a list of tupples
|
||||||
|
for the :py:func:`qubes.tools.print_table`.
|
||||||
|
|
||||||
|
If :program:`qvm-volume` is running in a TTY, it will ommit duplicate
|
||||||
|
data.
|
||||||
|
|
||||||
|
:param list vd_list: List of :py:class:`VolumeData` objects.
|
||||||
|
:param bool full: If set to true duplicate data is printed even when
|
||||||
|
running from TTY.
|
||||||
|
:returns: list of tupples
|
||||||
|
'''
|
||||||
|
output = []
|
||||||
|
output += [('POOL:VOLUME', 'VMNAME', 'VOLUME_NAME', 'REVERT_POSSIBLE')]
|
||||||
|
|
||||||
|
for volume in sorted(vd_list):
|
||||||
|
if volume.domains:
|
||||||
|
vmname, volume_name = volume.domains.pop()
|
||||||
|
output += [(str(volume), vmname, volume_name, volume.revisions)]
|
||||||
|
for tupple in volume.domains:
|
||||||
|
vmname, volume_name = tupple
|
||||||
|
if full or not sys.stdout.isatty():
|
||||||
|
output += [(str(volume), vmname, volume_name,
|
||||||
|
volume.revisions)]
|
||||||
|
else:
|
||||||
|
output += [('', vmname, volume_name, volume.revisions)]
|
||||||
|
else:
|
||||||
|
output += [(str(volume), "")]
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeData(object):
|
||||||
|
''' Wrapper object around :py:class:`qubes.storage.Volume`, mainly to track
|
||||||
|
the domains a volume is attached to.
|
||||||
|
'''
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
def __init__(self, volume):
|
||||||
|
self.pool = volume.pool
|
||||||
|
self.vid = volume.vid
|
||||||
|
if len(volume.revisions) > 0:
|
||||||
|
self.revisions = 'Yes'
|
||||||
|
else:
|
||||||
|
self.revisions = 'No'
|
||||||
|
self.domains = []
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return (self.pool, self.vid) < (other.pool, other.vid)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{!s}:{!s}".format(self.pool, self.vid)
|
||||||
|
|
||||||
|
|
||||||
|
def list_volumes(args):
|
||||||
|
''' Called by the parser to execute the qvm-volume list subcommand. '''
|
||||||
|
app = args.app
|
||||||
|
|
||||||
|
if hasattr(args, 'domains') and args.domains:
|
||||||
|
domains = args.domains
|
||||||
|
else:
|
||||||
|
domains = app.domains
|
||||||
|
volumes = [v for vm in domains for v in vm.volumes.values()]
|
||||||
|
|
||||||
|
if args.pools:
|
||||||
|
# only specified pools
|
||||||
|
volumes = [v for v in volumes if v.pool in args.pools]
|
||||||
|
|
||||||
|
if not args.internal: # hide internal volumes
|
||||||
|
volumes = [v for v in volumes if not v.internal]
|
||||||
|
|
||||||
|
vd_dict = {}
|
||||||
|
for volume in volumes:
|
||||||
|
volume_data = VolumeData(volume)
|
||||||
|
try:
|
||||||
|
vd_dict[volume.pool][volume.vid] = volume_data
|
||||||
|
except KeyError:
|
||||||
|
vd_dict[volume.pool] = {volume.vid: volume_data}
|
||||||
|
|
||||||
|
for domain in domains: # gather the domain names
|
||||||
|
try:
|
||||||
|
for name, volume in domain.volumes.items():
|
||||||
|
try:
|
||||||
|
volume_data = vd_dict[volume.pool][volume.vid]
|
||||||
|
volume_data.domains += [(domain.name, name)]
|
||||||
|
except KeyError:
|
||||||
|
# Skipping volume
|
||||||
|
continue
|
||||||
|
except AttributeError:
|
||||||
|
# Skipping domain without volumes
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(args, 'domains') and args.domains:
|
||||||
|
result = [x # reduce to only VolumeData with assigned domains
|
||||||
|
for p in vd_dict.values() for x in p.values()
|
||||||
|
if x.domains]
|
||||||
|
else:
|
||||||
|
result = [x for p in vd_dict.values() for x in p.values()]
|
||||||
|
qubesmgmt.tools.print_table(prepare_table(result, full=args.full))
|
||||||
|
|
||||||
|
|
||||||
|
def revert_volume(args):
|
||||||
|
''' Revert volume to previous state '''
|
||||||
|
volume = args.volume
|
||||||
|
app = args.app
|
||||||
|
try:
|
||||||
|
pool = app.pools[volume.pool]
|
||||||
|
pool.revert(volume)
|
||||||
|
except qubesmgmt.exc.StoragePoolException as e:
|
||||||
|
print(str(e), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def extend_volumes(args):
|
||||||
|
''' Called by the parser to execute the :program:`qvm-block extend`
|
||||||
|
subcommand
|
||||||
|
'''
|
||||||
|
volume = args.volume
|
||||||
|
size = qubesmgmt.utils.parse_size(args.size)
|
||||||
|
volume.resize(size)
|
||||||
|
|
||||||
|
|
||||||
|
def init_list_parser(sub_parsers):
|
||||||
|
''' Configures the parser for the :program:`qvm-block list` subcommand '''
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
list_parser = sub_parsers.add_parser('list', aliases=('ls', 'l'),
|
||||||
|
help='list storage volumes')
|
||||||
|
list_parser.add_argument('-p', '--pool', dest='pools',
|
||||||
|
action=qubesmgmt.tools.PoolsAction)
|
||||||
|
list_parser.add_argument('-i', '--internal', action='store_true',
|
||||||
|
help='Show internal volumes')
|
||||||
|
list_parser.add_argument(
|
||||||
|
'--full', action='store_true',
|
||||||
|
help='print full line for each POOL_NAME:VOLUME_ID & vm combination')
|
||||||
|
|
||||||
|
vm_name_group = qubesmgmt.tools.VmNameGroup(
|
||||||
|
list_parser, required=False, vm_action=qubesmgmt.tools.VmNameAction,
|
||||||
|
help='list volumes from specified domain(s)')
|
||||||
|
list_parser._mutually_exclusive_groups.append(vm_name_group)
|
||||||
|
list_parser.set_defaults(func=list_volumes)
|
||||||
|
|
||||||
|
|
||||||
|
def init_revert_parser(sub_parsers):
|
||||||
|
''' Add 'revert' action related options '''
|
||||||
|
revert_parser = sub_parsers.add_parser(
|
||||||
|
'revert', aliases=('rv', 'r'),
|
||||||
|
help='revert volume to previous revision')
|
||||||
|
revert_parser.add_argument(metavar='VM:VOLUME', dest='volume',
|
||||||
|
action=qubesmgmt.tools.VMVolumeAction)
|
||||||
|
revert_parser.set_defaults(func=revert_volume)
|
||||||
|
|
||||||
|
|
||||||
|
def init_extend_parser(sub_parsers):
|
||||||
|
''' Add 'extend' action related options '''
|
||||||
|
extend_parser = sub_parsers.add_parser(
|
||||||
|
"extend", help="extend volume from domain", aliases=('d', 'dt'))
|
||||||
|
extend_parser.add_argument(metavar='VM:VOLUME', dest='volume',
|
||||||
|
action=qubesmgmt.tools.VMVolumeAction)
|
||||||
|
extend_parser.add_argument('size', help='New size in bytes')
|
||||||
|
extend_parser.set_defaults(func=extend_volumes)
|
||||||
|
|
||||||
|
def get_parser():
|
||||||
|
'''Create :py:class:`argparse.ArgumentParser` suitable for
|
||||||
|
:program:`qvm-block`.
|
||||||
|
'''
|
||||||
|
parser = qubesmgmt.tools.QubesArgumentParser(description=__doc__,
|
||||||
|
want_app=True)
|
||||||
|
parser.register('action', 'parsers',
|
||||||
|
qubesmgmt.tools.AliasedSubParsersAction)
|
||||||
|
sub_parsers = parser.add_subparsers(
|
||||||
|
title='commands',
|
||||||
|
description="For more information see qvm-block command -h",
|
||||||
|
dest='command')
|
||||||
|
init_extend_parser(sub_parsers)
|
||||||
|
init_list_parser(sub_parsers)
|
||||||
|
init_revert_parser(sub_parsers)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None, app=None):
|
||||||
|
'''Main routine of :program:`qvm-block`.'''
|
||||||
|
parser = get_parser()
|
||||||
|
try:
|
||||||
|
args = parser.parse_args(args, app=app)
|
||||||
|
args.func(args)
|
||||||
|
except qubesmgmt.exc.QubesException as e:
|
||||||
|
parser.print_error(str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
Loading…
Reference in New Issue
Block a user