Browse Source

Merge branch 'devel-6'

* devel-6:
  qvm-ls: fix total VM size reporting
  doc: update manpage of qvm-service
  tools: qvm-service tool
  tests: too much copy&paste
  features: serialize True as '1'
  tools/qvm-start-gui: add --force-stubdomain options
  tools/qvm-shutdown: fix help message
  tools/qvm-shutdown: drop --force option, it isn't supported anymore
Marek Marczykowski-Górecki 7 years ago
parent
commit
f48321dd98

+ 16 - 21
doc/manpages/qvm-service.rst

@@ -8,6 +8,7 @@ Synopsis
 ========
 | :command:`qvm-service` [-l] <*vmname*>
 | :command:`qvm-service` [-e|-d|-D] <*vmname*> <*service*>
+| :command:`qvm-service` <*vmname*> <*service*> [on|off]
 
 Options
 =======
@@ -27,11 +28,19 @@ Options
 
     Disable service
 
-.. option:: --default, -D
+.. option:: --default, -D, --delete, --unset
 
     Reset service to its default state (remove from the list). Default state
     means "lets VM choose" and can depend on VM type (NetVM, AppVM etc).
 
+.. option:: --verbose, -v
+
+   increase verbosity
+
+.. option:: --quiet, -q
+
+   decrease verbosity
+
 Supported services
 ==================
 
@@ -50,11 +59,6 @@ meminfo-writer
         remove it (reset to default state), will be recreated with the rule: enabled
         if VM have no PCI devices assigned, otherwise disabled.
 
-qubes-dvm
-    Default: disabled
-
-    Used internally when creating DispVM savefile.
-
 qubes-firewall
     Default: enabled only in ProxyVM
 
@@ -72,14 +76,6 @@ qubes-network
     Expose network for other VMs. This includes enabling network forwarding,
     MASQUERADE, DNS redirection and basic firewall.
 
-qubes-netwatcher
-    Default: enabled only in ProxyVM
-
-    Monitor IP change notification from NetVM. When received, reload
-    qubes-firewall service (to force DNS resolution).
-
-    This service makes sense only with qubes-firewall enabled.
-
 qubes-update-check
     Default: enabled
 
@@ -103,12 +99,12 @@ network-manager
     Enable NetworkManager. Only VM with direct access to network device needs
     this service, but can be useful in ProxyVM to ease VPN setup.
 
-ntpd
+clocksync
     Default: disabled
 
-    Enable NTPD service. By default Qubes calls ntpdate every 6 minutes in
-    selected VM (aka ClockVM), then propagate the result using qrexec calls.
-    Enabling ntpd *do not* disable this behaviour.
+    Enable NTPD (or equivalent) service. If disabled, VM will sync clock with
+    selected VM (aka ClockVM) instead. ClockVM for particular VM can be set in
+    policy of qubes.GetDate service, using target= parameter.
 
 qubes-yum-proxy
     Deprecated name for qubes-updates-proxy.
@@ -130,9 +126,8 @@ updates-proxy-setup
 
     .. note::
 
-       this service is automatically enabled when you allow VM to access yum
-       proxy (in firewall settings) and disabled when you deny access to yum
-       proxy.
+       this service is automatically enabled when you allow VM to access updates
+       proxy and disabled when you deny access to updates proxy.
 
 disable-default-route
     Default: disabled

+ 1 - 6
doc/manpages/qvm-shutdown.rst

@@ -15,7 +15,7 @@
 Synopsis
 --------
 
-:command:`qvm-shutdown` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--force] [--wait] [--timeout *TIMEOUT*] [*VMNAME*]
+:command:`qvm-shutdown` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--wait] [--timeout *TIMEOUT*] [*VMNAME*]
 
 Options
 -------
@@ -40,11 +40,6 @@ Options
 
    exclude the qube from :option:`--all`
 
-.. option:: --force
-
-   force operation, even if may damage other VMs (eg. shutdown of network
-   provider)
-
 .. option:: --wait
 
    wait for the VMs to shut down

+ 5 - 1
doc/manpages/qvm-start-gui.rst

@@ -15,7 +15,7 @@
 Synopsis
 --------
 
-:command:`qvm-start-gui` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--watch] [--pidfile *PIDFILE*] [--notify-monitory-layout] [*VMNAME* [*VMNAME* ...]]
+:command:`qvm-start-gui` [-h] [--verbose] [--quiet] [--all] [--exclude *EXCLUDE*] [--watch] [--force-stubdomain] [--pidfile *PIDFILE*] [--notify-monitory-layout] [*VMNAME* [*VMNAME* ...]]
 
 Options
 -------
@@ -44,6 +44,10 @@ Options
 
    Keep watching for further domains startups, must be used with --all
 
+.. option:: --force-stubdomain
+
+   Start GUI to stubdomain-emulated VGA, even if gui-agent is running in the VM
+
 .. option:: --pidfile
 
    Pidfile path to create in --watch mode

+ 3 - 2
qubesadmin/features.py

@@ -42,9 +42,10 @@ class Features(object):
         self.vm.qubesd_call(self.vm.name, 'admin.vm.feature.Remove', key)
 
     def __setitem__(self, key, value):
-        if value is False:
+        if isinstance(value, bool):
             # False value needs to be serialized as empty string
-            self.vm.qubesd_call(self.vm.name, 'admin.vm.feature.Set', key, b'')
+            self.vm.qubesd_call(self.vm.name, 'admin.vm.feature.Set', key,
+                b'1' if value else b'')
         else:
             self.vm.qubesd_call(self.vm.name, 'admin.vm.feature.Set', key,
                 str(value).encode())

+ 2 - 0
qubesadmin/tests/backup/backupcompatibility.py

@@ -1350,6 +1350,8 @@ class TC_10_BackupCompatibility(qubesadmin.tests.backup.BackupTestCase):
             for feature, value in vm['features'].items():
                 if value is False:
                     value = ''
+                elif value is True:
+                    value = '1'
                 self.app.expected_calls[
                     (name, 'admin.vm.feature.Set', feature,
                     str(value).encode())] = b'0\0'

+ 1 - 1
qubesadmin/tests/features.py

@@ -78,7 +78,7 @@ class TC_00_Features(qubesadmin.tests.QubesTestCase):
 
     def test_021_set_bool(self):
         self.app.expected_calls[
-            ('test-vm', 'admin.vm.feature.Set', 'feature1', b'True')] = \
+            ('test-vm', 'admin.vm.feature.Set', 'feature1', b'1')] = \
             b'0\0'
         self.vm.features['feature1'] = True
         self.assertAllCalled()

+ 0 - 159
qubesadmin/tests/tools/qvm_features.py

@@ -91,162 +91,3 @@ class TC_00_qvm_features(qubesadmin.tests.QubesTestCase):
             self.assertEqual(stdout.getvalue(),
                 '')
         self.assertAllCalled()
-
-    def test_004_running_verbose(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Running\n' \
-            b'some-vm3 class=AppVM state=Halted\n'
-        self.app.expected_calls[
-            ('some-vm', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--running',
-                    'some-vm'], app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VM some-vm is running\n')
-        self.assertAllCalled()
-
-    def test_005_running_multi_verbose(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Running\n' \
-            b'some-vm3 class=AppVM state=Halted\n'
-        self.app.expected_calls[
-            ('some-vm', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n'
-        self.app.expected_calls[
-            ('some-vm2', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm2 class=AppVM state=Running\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--running',
-                    'some-vm', 'some-vm2'],
-                    app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VMs some-vm, some-vm2 are running\n')
-        self.assertAllCalled()
-
-    def test_006_running_multi_verbose2(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Running\n' \
-            b'some-vm3 class=AppVM state=Halted\n'
-        self.app.expected_calls[
-            ('some-vm', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n'
-        self.app.expected_calls[
-            ('some-vm2', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm2 class=AppVM state=Running\n'
-        self.app.expected_calls[
-            ('some-vm3', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm3 class=AppVM state=Halted\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--running',
-                    '--all'],
-                    app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VMs some-vm, some-vm2 are running\n')
-        self.assertAllCalled()
-
-    def test_007_not_running_verbose(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Running\n' \
-            b'some-vm3 class=AppVM state=Halted\n'
-        self.app.expected_calls[
-            ('some-vm3', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm3 class=AppVM state=Halted\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--running',
-                    'some-vm3'],
-                    app=self.app),
-                1)
-            self.assertEqual(stdout.getvalue(),
-                'None of given VM is running\n')
-        self.assertAllCalled()
-
-    def test_008_paused(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Paused\n' \
-            b'some-vm3 class=AppVM state=Halted\n'
-        self.app.expected_calls[
-            ('some-vm2', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm2 class=AppVM state=Paused\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--paused',
-                    'some-vm2'],
-                    app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VM some-vm2 is paused\n')
-        self.assertAllCalled()
-
-    def test_009_paused_multi(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Paused\n' \
-            b'some-vm3 class=AppVM state=Halted\n'
-        self.app.expected_calls[
-            ('some-vm2', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm2 class=AppVM state=Paused\n'
-        self.app.expected_calls[
-            ('some-vm', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--paused',
-                    'some-vm2', 'some-vm'],
-                    app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VM some-vm2 is paused\n')
-        self.assertAllCalled()
-
-
-    def test_010_template(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Paused\n' \
-            b'some-vm3 class=TemplateVM state=Halted\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--template',
-                    'some-vm3'],
-                    app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VM some-vm3 is a template\n')
-        self.assertAllCalled()
-
-    def test_011_template_multi(self):
-        self.app.expected_calls[
-            ('dom0', 'admin.vm.List', None, None)] = \
-            b'0\x00some-vm class=AppVM state=Running\n' \
-            b'some-vm2 class=AppVM state=Paused\n' \
-            b'some-vm3 class=TemplateVM state=Halted\n'
-        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
-            self.assertEqual(
-                qubesadmin.tools.qvm_check.main(['--template',
-                    'some-vm2', 'some-vm3'],
-                    app=self.app),
-                0)
-            self.assertEqual(stdout.getvalue(),
-                'VM some-vm3 is a template\n')
-        self.assertAllCalled()
-

+ 203 - 0
qubesadmin/tests/tools/qvm_service.py

@@ -0,0 +1,203 @@
+# -*- 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 unittest
+
+import qubesadmin.tests
+import qubesadmin.tools.qvm_service
+
+
+class TC_00_qvm_service(qubesadmin.tests.QubesTestCase):
+    def test_000_list(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.List', None, None)] = \
+            b'0\x00feature1\nservice.service1\nservice.service2\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Get', 'service.service1', None)] = \
+            b'0\x00value1'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Get', 'service.service2', None)] = \
+            b'0\x00'
+        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(['some-vm'], app=self.app),
+                0)
+            self.assertEqual(stdout.getvalue(),
+                'service1  on\n'
+                'service2  off\n')
+        self.assertAllCalled()
+
+    def test_001_list_l(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.List', None, None)] = \
+            b'0\x00feature1\nservice.service1\nservice.service2\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Get', 'service.service1', None)] = \
+            b'0\x00value1'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Get', 'service.service2', None)] = \
+            b'0\x00'
+        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(['-l', 'some-vm'],
+                    app=self.app),
+                0)
+            self.assertEqual(stdout.getvalue(),
+                'service1  on\n'
+                'service2  off\n')
+        self.assertAllCalled()
+
+    def test_002_enable(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Set',
+             'service.service1', b'1')] = b'0\x00'
+        self.assertEqual(
+            qubesadmin.tools.qvm_service.main(['some-vm', 'service1',
+                'on'],
+                app=self.app),
+            0)
+        self.assertAllCalled()
+
+    def test_003_enable_opt(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Set',
+             'service.service1', b'1')] = b'0\x00'
+        self.assertEqual(
+            qubesadmin.tools.qvm_service.main(['--enable', 'some-vm',
+                'service1'],
+                app=self.app),
+            0)
+        self.assertAllCalled()
+
+    @unittest.expectedFailure
+    def test_004_enable_opt_mixed(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Set',
+             'service.service1', b'1')] = b'0\x00'
+        with self.assertNotRaises(SystemExit):
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(
+                    ['some-vm', '--enable', 'service1'],
+                    app=self.app),
+                0)
+        self.assertAllCalled()
+
+    def test_005_disable(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Set',
+             'service.service1', b'')] = b'0\x00'
+        self.assertEqual(
+            qubesadmin.tools.qvm_service.main(
+                ['some-vm', 'service1', 'off'],
+                app=self.app),
+            0)
+        self.assertAllCalled()
+
+    def test_006_disable_opt(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Set',
+             'service.service1', b'')] = b'0\x00'
+        with self.assertNotRaises(SystemExit):
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(
+                    ['--disable', 'some-vm', 'service1'],
+                    app=self.app),
+                0)
+        self.assertAllCalled()
+
+    def test_007_get(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Get', 'service.service3', None)] = \
+            b'0\x00value2 with spaces'
+        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(['some-vm', 'service3'],
+                    app=self.app),
+                0)
+            self.assertEqual(stdout.getvalue(),
+                'on\n')
+        self.assertAllCalled()
+
+    def test_008_del(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Remove', 'service.srv4', None)] = \
+            b'0\x00'
+        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(
+                    ['--unset', 'some-vm', 'srv4'],
+                    app=self.app),
+                0)
+            self.assertEqual(stdout.getvalue(),
+                '')
+        self.assertAllCalled()
+
+    def test_009_del_legacy(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        self.app.expected_calls[
+            ('some-vm', 'admin.vm.feature.Remove', 'service.srv4', None)] = \
+            b'0\x00'
+        with qubesadmin.tests.tools.StdoutBuffer() as stdout:
+            self.assertEqual(
+                qubesadmin.tools.qvm_service.main(
+                    ['--default', 'some-vm', 'srv4'],
+                    app=self.app),
+                0)
+            self.assertEqual(stdout.getvalue(),
+                '')
+        self.assertAllCalled()
+
+    def test_010_set_invalid(self):
+        self.app.expected_calls[
+            ('dom0', 'admin.vm.List', None, None)] = \
+            b'0\x00some-vm class=AppVM state=Running\n'
+        with self.assertRaises(SystemExit):
+            qubesadmin.tools.qvm_service.main(
+                ['some-vm', 'service1', 'invalid-value'],
+                app=self.app),
+        self.assertAllCalled()

+ 2 - 2
qubesadmin/tests/tools/qvm_template_postprocess.py

@@ -243,7 +243,7 @@ class TC_00_qvm_template_postprocess(qubesadmin.tests.QubesTestCase):
         self.app.expected_calls[
             ('test-vm', 'admin.vm.property.Reset', 'netvm', None)] = b'0\0'
         self.app.expected_calls[
-            ('test-vm', 'admin.vm.feature.Set', 'qrexec', b'True')] = b'0\0'
+            ('test-vm', 'admin.vm.feature.Set', 'qrexec', b'1')] = b'0\0'
         self.app.expected_calls[
             ('test-vm', 'admin.vm.Start', None, None)] = b'0\0'
         self.app.expected_calls[
@@ -293,7 +293,7 @@ class TC_00_qvm_template_postprocess(qubesadmin.tests.QubesTestCase):
         self.app.expected_calls[
             ('test-vm', 'admin.vm.property.Reset', 'netvm', None)] = b'0\0'
         self.app.expected_calls[
-            ('test-vm', 'admin.vm.feature.Set', 'qrexec', b'True')] = b'0\0'
+            ('test-vm', 'admin.vm.feature.Set', 'qrexec', b'1')] = b'0\0'
         self.app.expected_calls[
             ('test-vm', 'admin.vm.Start', None, None)] = b'0\0'
         self.app.expected_calls[

+ 1 - 1
qubesadmin/tools/qvm_ls.py

@@ -351,7 +351,7 @@ Column('MEMORY',
     doc='Memory currently used by VM')
 
 Column('DISK',
-    attr=(lambda vm: vm.storage.get_disk_utilization() / 1024 / 1024),
+    attr=(lambda vm: vm.get_disk_utilization() // 1024 // 1024),
     doc='Total disk utilisation.')
 
 

+ 136 - 0
qubesadmin/tools/qvm_service.py

@@ -0,0 +1,136 @@
+# coding=utf-8
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2010-2016  Joanna Rutkowska <joanna@invisiblethingslab.com>
+# Copyright (C) 2016       Wojtek Porczyk <woju@invisiblethingslab.com>
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''qvm-service - Manage domain's services'''
+
+from __future__ import print_function
+
+import argparse
+import sys
+
+import qubesadmin
+import qubesadmin.exc
+import qubesadmin.tools
+
+parser = qubesadmin.tools.QubesArgumentParser(
+    vmname_nargs=1,
+    argument_default=argparse.SUPPRESS,
+    description='manage domain\'s services')
+
+parser.add_argument('service', metavar='FEATURE',
+    action='store', nargs='?',
+    help='name of the feature')
+
+parser.add_argument('value', metavar='VALUE',
+    action='store', nargs='?',
+    help='new value of the service')
+
+parser.add_argument('--unset', '--default', '--delete', '-D',
+    dest='delete', default=False,
+    action='store_true',
+    help='unset service (default to VM preference)')
+
+parser.add_argument('--list', '-l',
+    dest='list',
+    action='store_true',
+    help='list services (default action)')
+
+parser.add_argument('--enable', '-e',
+    dest='value',
+    action='store_const', const='1',
+    help='enable service')
+
+parser.add_argument('--disable', '-d',
+    dest='value',
+    action='store_const', const='0',
+    help='disable service')
+
+def parse_bool(value):
+    '''Convert string value to bool according to well known representations
+
+    It accepts (case-insensitive) ``'0'``, ``'no'`` and ``false`` as
+        :py:obj:`False` and ``'1'``, ``'yes'`` and ``'true'`` as
+        :py:obj:`True`.
+    '''
+    if isinstance(value, str):
+        lcvalue = value.lower()
+        if lcvalue in ('0', 'no', 'false', 'off'):
+            return False
+        if lcvalue in ('1', 'yes', 'true', 'on'):
+            return True
+        raise qubesadmin.exc.QubesValueError(
+            'Invalid literal for boolean value: {!r}'.format(value))
+
+    return bool(value)
+
+
+def main(args=None, app=None):
+    '''Main routine of :program:`qvm-features`.
+
+    :param list args: Optional arguments to override those delivered from \
+        command line.
+    '''
+
+    args = parser.parse_args(args, app=app)
+    vm = args.domains[0]
+
+    if not hasattr(args, 'service'):
+        if args.delete:
+            parser.error('--unset requires a feature')
+
+        services = [(feat[len('service.'):],
+            'on' if vm.features[feat] else 'off') for feat in
+            vm.features if feat.startswith('service.')]
+        qubesadmin.tools.print_table(services)
+
+    elif args.delete:
+        if hasattr(args, 'value'):
+            parser.error('cannot both set and unset a value')
+        try:
+            del vm.features['service.' + args.service]
+        except KeyError:
+            pass
+        except qubesadmin.exc.QubesException as err:
+            parser.error_runtime(str(err))
+
+    elif hasattr(args, 'value'):
+        try:
+            vm.features['service.' + args.service] = parse_bool(args.value)
+        except qubesadmin.exc.QubesException as err:
+            parser.error_runtime(str(err))
+
+    else:
+        try:
+            print('on' if vm.features['service.' + args.service] else 'off')
+            return 0
+        except KeyError:
+            return 1
+        except qubesadmin.exc.QubesException as err:
+            parser.error_runtime(str(err))
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 1 - 6
qubesadmin/tools/qvm_shutdown.py

@@ -40,11 +40,6 @@ import qubesadmin.exc
 parser = qubesadmin.tools.QubesArgumentParser(
     description=__doc__, vmname_nargs='+')
 
-parser.add_argument('--force',
-    action='store_true', default=False,
-    help='force operation, even if may damage other VMs (eg. shutdown of'
-        ' network provider)')
-
 parser.add_argument('--wait',
     action='store_true', default=False,
     help='wait for the VMs to shut down')
@@ -53,7 +48,7 @@ parser.add_argument('--timeout',
     action='store', type=float,
     default=60,
     help='timeout after which domains are killed when using --wait'
-        ' (default: %d)')
+        ' (default: %(default)d)')
 
 
 def main(args=None, app=None):  # pylint: disable=missing-docstring

+ 5 - 1
qubesadmin/tools/qvm_start_gui.py

@@ -347,6 +347,9 @@ parser = qubesadmin.tools.QubesArgumentParser(
     description='start GUI for qube(s)', vmname_nargs='*')
 parser.add_argument('--watch', action='store_true',
     help='Keep watching for further domains startups, must be used with --all')
+parser.add_argument('--force-stubdomain', action='store_true',
+    help='Start GUI to stubdomain-emulated VGA, even if gui-agent is running '
+         'in the VM')
 parser.add_argument('--pidfile', action='store', default=pidfile_path,
     help='Pidfile path to create in --watch mode')
 parser.add_argument('--notify-monitor-layout', action='store_true',
@@ -400,7 +403,8 @@ def main(args=None):
         tasks = []
         for vm in args.domains:
             if vm.is_running():
-                tasks.append(asyncio.ensure_future(launcher.start_gui(vm)))
+                tasks.append(asyncio.ensure_future(launcher.start_gui(
+                    vm, force_stubdom=args.force_stubdomain)))
         if tasks:
             loop.run_until_complete(asyncio.wait(tasks))
         loop.stop()

+ 4 - 0
qubesadmin/vm/__init__.py

@@ -257,6 +257,10 @@ class QubesVM(qubesadmin.base.PropertyHolder):
                     vm=self.name, vm_name=volname)
         return self._volumes
 
+    def get_disk_utilization(self):
+        '''Get total disk usage of the VM'''
+        return sum(vol.usage for vol in self.volumes.values())
+
     def run_service(self, service, **kwargs):
         '''Run service on this VM