Browse Source

api/admin: make admin.vm.Console call go through qubesd

Ask qubesd for admin.vm.Console call. This allows to intercept it with
admin-permission event. While at it, extract tty path extraction to
python, where libvirt domain object is already available.

Fixes QubesOS/qubes-issues#5030
Marek Marczykowski-Górecki 4 years ago
parent
commit
b6c4f8456f
3 changed files with 77 additions and 4 deletions
  1. 16 4
      qubes-rpc/admin.vm.Console
  2. 18 0
      qubes/api/admin.py
  3. 43 0
      qubes/tests/api_admin.py

+ 16 - 4
qubes-rpc/admin.vm.Console

@@ -1,11 +1,23 @@
 #!/bin/bash
 
-# TODO: handle 'admin-permission' event for qubesd
-
 lock="/var/run/qubes/$QREXEC_REQUESTED_TARGET.terminal.lock"
 
-qvm-check --quiet --running "$QREXEC_REQUESTED_TARGET" > /dev/null 2>&1 || { echo "Error: domain '$QREXEC_REQUESTED_TARGET' does not exist or is not running"; exit 1; }
+# use temporary file, because env variables deal poorly with \0 inside
+tmpfile=$(mktemp)
+trap "rm -f $tmpfile" EXIT
+qubesd-query -e \
+        "$QREXEC_REMOTE_DOMAIN" \
+        "admin.vm.Console" \
+        "$QREXEC_REQUESTED_TARGET" \
+        "$1" >$tmpfile
+
+# exit if qubesd returned an error (not '0\0')
+if [ "$(head -c 2 $tmpfile | xxd -p)" != "3000" ]; then
+    cat "$tmpfile"
+    exit 1
+fi
+path=$(tail -c +3 "$tmpfile")
 
 # Create an exclusive lock to ensure that multiple qubes cannot access to the same socket
 # In the case of multiple qrexec calls it returns a specific exit code
-sudo flock -n -E 200 -x "$lock" socat - OPEN:"$(virsh -c xen ttyconsole "$QREXEC_REQUESTED_TARGET")"
+sudo flock -n -E 200 -x "$lock" socat - OPEN:"$path"

+ 18 - 0
qubes/api/admin.py

@@ -29,6 +29,7 @@ import string
 import subprocess
 
 import libvirt
+import lxml.etree
 import pkg_resources
 import yaml
 
@@ -575,6 +576,23 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
         self.app.save()
 
+    @qubes.api.method('admin.vm.Console', no_payload=True,
+        scope='local', write=True)
+    @asyncio.coroutine
+    def vm_console(self):
+        self.enforce(not self.arg)
+
+        self.fire_event_for_permission()
+
+        if not self.dest.is_running():
+            raise qubes.exc.QubesVMNotRunningError(self.dest)
+
+        xml_desc = lxml.etree.fromstring(self.dest.libvirt_domain.XMLDesc())
+        ttypath = xml_desc.xpath('string(/domain/devices/console/@tty)')
+        # this value is returned to /etc/qubes-rpc/admin.vm.Console script,
+        # which will call socat on it
+        return ttypath
+
     @qubes.api.method('admin.pool.List', no_payload=True,
         scope='global', read=True)
     @asyncio.coroutine

+ 43 - 0
qubes/tests/api_admin.py

@@ -2475,6 +2475,47 @@ class TC_00_VMs(AdminAPITestCase):
                 b'test-vm1', b'private', b'abc')
         self.assertFalse(self.app.save.called)
 
+    def test_690_vm_console(self):
+        self.vm._libvirt_domain = unittest.mock.Mock()
+        xml_desc = (
+            '<domain type=\'xen\' id=\'42\'>\n'
+            '<name>test-vm1</name>\n'
+            '<devices>\n'
+            '<console type=\'pty\' tty=\'/dev/pts/42\'>\n'
+            '<source path=\'/dev/pts/42\'/>\n'
+            '<target type=\'xen\' port=\'0\'/>\n'
+            '</console>\n'
+            '</devices>\n'
+            '</domain>\n'
+        )
+        self.vm._libvirt_domain.configure_mock(
+            **{'XMLDesc.return_value': xml_desc,
+               'isActive.return_value': True}
+        )
+        self.app.vmm.configure_mock(offline_mode=False)
+        value = self.call_mgmt_func(b'admin.vm.Console', b'test-vm1')
+        self.assertEqual(value, '/dev/pts/42')
+
+    def test_691_vm_console_not_running(self):
+        self.vm._libvirt_domain = unittest.mock.Mock()
+        xml_desc = (
+            '<domain type=\'xen\' id=\'42\'>\n'
+            '<name>test-vm1</name>\n'
+            '<devices>\n'
+            '<console type=\'pty\' tty=\'/dev/pts/42\'>\n'
+            '<source path=\'/dev/pts/42\'/>\n'
+            '<target type=\'xen\' port=\'0\'/>\n'
+            '</console>\n'
+            '</devices>\n'
+            '</domain>\n'
+        )
+        self.vm._libvirt_domain.configure_mock(
+            **{'XMLDesc.return_value': xml_desc,
+               'isActive.return_value': False}
+        )
+        with self.assertRaises(qubes.exc.QubesVMNotRunningError):
+            self.call_mgmt_func(b'admin.vm.Console', b'test-vm1')
+
     def test_990_vm_unexpected_payload(self):
         methods_with_no_payload = [
             b'admin.vm.List',
@@ -2505,6 +2546,7 @@ class TC_00_VMs(AdminAPITestCase):
             b'admin.vm.Pause',
             b'admin.vm.Unpause',
             b'admin.vm.Kill',
+            b'admin.vm.Console',
             b'admin.Events',
             b'admin.vm.feature.List',
             b'admin.vm.feature.Get',
@@ -2549,6 +2591,7 @@ class TC_00_VMs(AdminAPITestCase):
             b'admin.vm.Pause',
             b'admin.vm.Unpause',
             b'admin.vm.Kill',
+            b'admin.vm.Console',
             b'admin.Events',
             b'admin.vm.feature.List',
         ]