mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 19:56:48 +00:00
feat(instances): a running VM gets its root password and SSH key through its guest agent
The Access tab's Root Password and SSH Keys worked only on a shut-off VM, through gstfsd on the compute. A running VM whose QEMU guest agent is connected now gets them through the agent: guest-set-user-password with the SHA-512 crypt hash, and guest-ssh-add-authorized-keys adding the key to root's keys. A shut-off VM still goes through gstfsd; any other VM is refused with "Shut the VM down, or start its QEMU guest agent, and try again", and the buttons are disabled. Each is one agent command with a 10 s timeout. libvirt's own calls for them (setUserPassword, authorizedSSHKeysSet) wait for a hung agent indefinitely, holding a worker and the VM's lock, and the domain's agent timeout would change every other agent call of the VM too. The agent's or libvirt's error is shown as is through error_text, so users who do not see the hosts get no connection details, and the attempt is logged as failed. The README names the SELinux boolean a RHEL-family guest needs before its agent may add SSH keys.
This commit is contained in:
parent
85b5c769e9
commit
75fcc6f6c0
7 changed files with 176 additions and 16 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
import contextlib
|
||||
import io
|
||||
import ipaddress
|
||||
|
|
@ -73,6 +74,8 @@ PERSISTENT_XML = VIR_DOMAIN_XML_INACTIVE | VIR_DOMAIN_XML_SECURE
|
|||
SNAPSHOT_NAME = re.compile(r"[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}")
|
||||
# the longest window wvmInstance.usage() averages over
|
||||
USAGE_MAX_WINDOW = 30
|
||||
# seconds the guest agent has to set a password or add a key
|
||||
AGENT_TIMEOUT = 10
|
||||
# A running VM is cloned with a push backup that keeps the VM alive when the
|
||||
# guest shuts down during the copy: libvirt 11.10.
|
||||
LIVE_CLONE_MIN_LIBVIRT = 11_010_000
|
||||
|
|
@ -2522,6 +2525,24 @@ class wvmInstance(wvmConnect):
|
|||
|
||||
return util.get_xml_path(self._XMLDesc(0), func=_get_agent)
|
||||
|
||||
def _agent_command(self, command, arguments):
|
||||
"""One guest agent command that waits AGENT_TIMEOUT seconds at most:
|
||||
libvirt's own calls for it wait for a hung agent indefinitely, and
|
||||
the domain's agent timeout is shared by every agent call."""
|
||||
request = json.dumps({"execute": command, "arguments": arguments})
|
||||
qemuAgentCommand(self.instance, request, AGENT_TIMEOUT, 0)
|
||||
|
||||
def set_user_password(self, user, crypted):
|
||||
"""Set a user's password in the running guest through its agent;
|
||||
crypted is a crypt(3) hash."""
|
||||
password = base64.b64encode(crypted.encode()).decode()
|
||||
self._agent_command("guest-set-user-password", {"username": user, "password": password, "crypted": True})
|
||||
|
||||
def add_authorized_key(self, user, key):
|
||||
"""Add an SSH public key to the user's authorized keys in the running
|
||||
guest through its agent."""
|
||||
self._agent_command("guest-ssh-add-authorized-keys", {"username": user, "keys": [key], "reset": False})
|
||||
|
||||
def is_agent_ready(self):
|
||||
"""
|
||||
Return connected state of an agent.
|
||||
|
|
|
|||
49
vrtManager/tests/test_agent_access.py
Normal file
49
vrtManager/tests/test_agent_access.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""A running guest's root password and SSH keys through its QEMU guest
|
||||
agent: the password as a crypt(3) hash, the key added to those it has. Each
|
||||
is one agent command with a timeout: libvirt's own calls for them would wait
|
||||
for a hung agent indefinitely, and setting the domain's agent timeout would
|
||||
change it for every other agent call too."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
if not settings.configured:
|
||||
settings.configure(MAC_OUI="52:54:10")
|
||||
|
||||
from libvirt import libvirtError
|
||||
from vrtManager import instance as module
|
||||
from vrtManager.instance import wvmInstance
|
||||
|
||||
|
||||
class AgentAccessTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.vm = wvmInstance.__new__(wvmInstance) # no connection: only these calls
|
||||
self.vm.instance = MagicMock()
|
||||
patcher = patch.object(module, "qemuAgentCommand", return_value='{"return":{}}')
|
||||
self.agent = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def sent(self):
|
||||
(dom, command, timeout, flags), = [c.args for c in self.agent.call_args_list]
|
||||
self.assertIs(dom, self.vm.instance)
|
||||
self.assertEqual((timeout, flags), (10, 0))
|
||||
return json.loads(command)
|
||||
|
||||
def test_the_password_goes_as_a_hash(self):
|
||||
self.vm.set_user_password("root", "$6$salt$hash")
|
||||
self.assertEqual(self.sent(), {"execute": "guest-set-user-password", "arguments": {
|
||||
"username": "root", "password": base64.b64encode(b"$6$salt$hash").decode(), "crypted": True}})
|
||||
|
||||
def test_the_key_is_added_to_the_users_keys(self):
|
||||
self.vm.add_authorized_key("root", "ssh-ed25519 AAAA laptop")
|
||||
self.assertEqual(self.sent(), {"execute": "guest-ssh-add-authorized-keys", "arguments": {
|
||||
"username": "root", "keys": ["ssh-ed25519 AAAA laptop"], "reset": False}})
|
||||
|
||||
def test_the_agents_error_is_passed_on(self):
|
||||
self.agent.side_effect = libvirtError("guest agent didn't respond to command within '10' seconds")
|
||||
with self.assertRaisesRegex(libvirtError, "within '10' seconds"):
|
||||
self.vm.set_user_password("root", "$6$salt$hash")
|
||||
Loading…
Add table
Add a link
Reference in a new issue