1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

fix(gstfsd): find the guest OS by inspection, answer once, and stop a change at its deadline

gstfsd mounted every partition it listed: a root on LVM was never found,
a disk with two OS roots got the change twice and two concatenated
replies, and no root, an error or a request longer than 1024 bytes ended
the connection without a reply. It now lets libguestfs inspect the guest,
mounts its one OS the way the guest does (a broken filesystem off the
changed path is left out), and sends exactly one reply, keeping the first
error. The root entry is found on any line of /etc/shadow and must be
unique; a key needs no shadow file. A request is at most 64 KiB. Each
change runs in a worker process with a directly started appliance; both
are killed when the request is not done 100 s after gstfsd accepted it
(GSTFSD_DEADLINE may only lower that), before the panel's 120 s timeout
releases the VM, or when the panel's connection ends, and the reply waits
until they are gone, so nothing touches the disk once the panel has given
up. A worker that dies says why. The panel
connects over IPv4 or IPv6, sends the whole request and reads the reply
until gstfsd closes.
This commit is contained in:
catborise 2026-10-07 22:53:48 +03:00
parent bb6ad06697
commit 5ee13f305d
5 changed files with 610 additions and 92 deletions

View file

@ -28,10 +28,12 @@ class SetRootPassTestCase(TestCase):
def test_set_root_pass_post(self, mock_proxy, mock_socket_cls):
# Renamed on the host: gstfsd must get the name of this UUID's domain
mock_proxy.instance.name.return_value = "renamed-on-host"
mock_proxy.get_status.return_value = 5 # status: running
mock_proxy.get_status.return_value = 5 # shut off
mock_sock = MagicMock()
mock_socket_cls.return_value = mock_sock
mock_sock.recv.return_value = json.dumps({"return": "success"}).encode()
replies = iter([json.dumps({"return": "success"}).encode(), b""] * 2)
# one reply, then gstfsd closes the connection
mock_sock.recv.side_effect = lambda size: next(replies)
response = self.client.post(
reverse("instances:rootpasswd", args=[self.instance.id]),
@ -41,8 +43,8 @@ class SetRootPassTestCase(TestCase):
self.assertEqual(response.status_code, 302)
# Check socket sent the command with hashed password
self.assertTrue(mock_sock.send.called)
sent_bytes = mock_sock.send.call_args[0][0]
self.assertTrue(mock_sock.sendall.called)
sent_bytes = mock_sock.sendall.call_args[0][0]
data = json.loads(sent_bytes.decode())
self.assertEqual(data["action"], "password")
self.assertEqual(data["vname"], "renamed-on-host")
@ -65,13 +67,15 @@ class SetRootPassTestCase(TestCase):
mock_proxy.get_status.return_value = 5
mock_sock = MagicMock()
mock_socket_cls.return_value = mock_sock
mock_sock.recv.return_value = json.dumps({"return": "success"}).encode()
replies = iter([json.dumps({"return": "success"}).encode(), b""] * 2)
# one reply, then gstfsd closes the connection
mock_sock.recv.side_effect = lambda size: next(replies)
url = reverse("instances:rootpasswd", args=[self.instance.id])
self.client.post(url, {"passwd": "samePassword1"})
self.client.post(url, {"passwd": "samePassword1"})
hashes = [json.loads(c[0][0].decode())["passwd"] for c in mock_sock.send.call_args_list]
hashes = [json.loads(c[0][0].decode())["passwd"] for c in mock_sock.sendall.call_args_list]
self.assertEqual(len(hashes), 2)
self.assertNotEqual(hashes[0], hashes[1])