-
Notifications
You must be signed in to change notification settings - Fork 77
/
check-machines-consoles
executable file
·256 lines (200 loc) · 10.8 KB
/
check-machines-consoles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
# This file is part of Cockpit.
#
# Copyright (C) 2021 Red Hat, Inc.
#
# Cockpit 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.
#
# Cockpit 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 Cockpit; If not, see <http://www.gnu.org/licenses/>.
import time
import machineslib
import testlib
@testlib.nondestructive
class TestMachinesConsoles(machineslib.VirtualMachinesCase):
def waitDownloadFile(self, filename: str, expected_size: int | None = None, content: str | None = None) -> None:
b = self.browser
filepath = b.driver.download_dir / filename
# Big downloads can take a while
testlib.wait(filepath.exists, tries=120)
if expected_size is not None:
testlib.wait(lambda: filepath.stat().st_size == expected_size)
if content is not None:
self.assertEqual(filepath.read_text(), content)
@testlib.skipImage('SPICE not supported on RHEL', "rhel-*", "centos-*")
def testExternalConsole(self):
b = self.browser
self.createVm("subVmTest1", graphics="spice")
self.login_and_go("/machines")
self.waitPageInit()
self.waitVmRow("subVmTest1")
b.wait_in_text("#vm-subVmTest1-system-state", "Running") # running or paused
self.goToVmPage("subVmTest1")
# since VNC is not defined for this VM, the view for "Desktop Viewer" is rendered by default
b.wait_in_text(".pf-v5-c-console__manual-connection dl > div:first-child dd", "127.0.0.1")
b.wait_in_text(".pf-v5-c-console__manual-connection dl > div:nth-child(2) dd", "5900")
b.allow_download()
b.click(".pf-v5-c-console__remote-viewer-launch-vv") # "Launch Remote Viewer" button
content = """[virt-viewer]
type=spice
host=127.0.0.1
port=5900
delete-this-file=1
fullscreen=0
[...............................GraphicsConsole]
"""
# HACK: Due to a bug in cockpit-machines, Firefox downloads the desktop
# viewer file as a randomly generated file while chromium as
# "download". As we want to download this as "$vmName.vv" in the end
# work around the issue for now.
if b.browser == "chromium":
self.waitDownloadFile("download", content=content)
else:
b.wait_visible("#dynamically-generated-file") # is .vv file generated for download?
self.assertEqual(b.attr("#dynamically-generated-file", "href"),
u"data:application/x-virt-viewer,%5Bvirt-viewer%5D%0Atype%3Dspice%0Ahost%3D127.0.0.1%0Aport%3D5900%0Adelete-this-file%3D1%0Afullscreen%3D0%0A")
# Go to the expanded console view
b.click("button:contains(Expand)")
# Check "More information"
b.click('.pf-v5-c-console__remote-viewer .pf-v5-c-expandable-section__toggle')
b.wait_in_text('.pf-v5-c-expandable-section__content',
'Clicking "Launch remote viewer" will download')
b.assert_pixels("#vm-subVmTest1-consoles-page", "vm-details-console-external", skip_layouts=["rtl"])
def testInlineConsole(self, urlroot=""):
b = self.browser
args = self.createVm("subVmTest1", "vnc")
if urlroot != "":
self.machine.write("/etc/cockpit/cockpit.conf", f"[WebService]\nUrlRoot={urlroot}")
self.login_and_go("/machines", urlroot=urlroot)
self.waitPageInit()
self.waitVmRow("subVmTest1")
b.wait_in_text("#vm-subVmTest1-system-state", "Running") # running or paused
self.goToVmPage("subVmTest1")
# since VNC is defined for this VM, the view for "In-Browser Viewer" is rendered by default
b.wait_visible(".pf-v5-c-console__vnc canvas")
# make sure the log file is full - then empty it and reboot the VM - the log file should fill up again
self.waitGuestBooted(args['logfile'])
self.machine.execute(f"echo '' > {args['logfile']}")
b.click("#subVmTest1-system-vnc-sendkey")
b.click("#ctrl-alt-Delete")
self.waitLogFile(args['logfile'], "reboot: Restarting system")
def testInlineConsoleWithUrlRoot(self, urlroot=""):
self.testInlineConsole(urlroot="/webcon")
def testSerialConsole(self):
b = self.browser
m = self.machine
name = "vmWithSerialConsole"
self.createVm(name, graphics='vnc', ptyconsole=True)
self.login_and_go("/machines")
self.waitPageInit()
self.waitVmRow(name)
self.goToVmPage(name)
b.wait_in_text(f"#vm-{name}-system-state", "Running")
b.click("#pf-v5-c-console__type-selector")
b.wait_visible("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.click("#SerialConsole button")
b.wait_not_present("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
# In case the OS already finished booting, press Enter into the console to re-trigger the login prompt
# Sometimes, pressing Enter one time doesn't take effect, so loop to press Enter to make sure
# the console has accepted it.
for _ in range(0, 60):
b.focus(f"#{name}-terminal .xterm-accessibility-tree")
b.key("Enter")
if "Welcome to Alpine Linux" in b.text(f"#{name}-terminal .xterm-accessibility-tree"):
break
time.sleep(1)
# Make sure the content of console is expected
testlib.wait(lambda: "Welcome to Alpine Linux" in b.text(f"#{name}-terminal .xterm-accessibility-tree"))
b.click(f"#{name}-serialconsole-disconnect")
b.wait_text(f"#{name}-terminal", "Disconnected from serial console. Click the connect button.")
b.click(f"#{name}-serialconsole-connect")
b.wait_in_text(f"#{name}-terminal .xterm-accessibility-tree > div:nth-child(1)",
f"Connected to domain '{name}'")
b.click("button:contains(Expand)")
b.assert_pixels("#vm-vmWithSerialConsole-consoles-page", "vm-details-console-serial",
ignore=[".pf-v5-c-console__vnc"], skip_layouts=["rtl"])
# Add a second serial console
m.execute("""
virsh destroy vmWithSerialConsole;
virt-xml --add-device vmWithSerialConsole --console pty,target_type=virtio;
virsh start vmWithSerialConsole""")
b.click("#pf-v5-c-console__type-selector")
b.wait_visible("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.click("li:contains('Serial console (serial0)') button")
b.wait(lambda: m.execute("ps aux | grep 'virsh -c qemu:///system console vmWithSerialConsole serial0'"))
b.click("#pf-v5-c-console__type-selector")
b.click("li:contains('Serial console (console1)') button")
b.wait(lambda: m.execute("ps aux | grep 'virsh -c qemu:///system console vmWithSerialConsole console1'"))
# Add multiple serial consoles
# Remove all console firstly
m.execute("virsh destroy vmWithSerialConsole")
m.execute("virt-xml --remove-device vmWithSerialConsole --console all")
# Add console1 ~ console5
m.execute("""
for i in {1..5}; do
virt-xml vmWithSerialConsole --add-device --console pty,target.type=virtio;
done
virsh start vmWithSerialConsole
""")
for i in range(0, 6):
b.click("#pf-v5-c-console__type-selector")
b.wait_visible("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.click(f'li:contains(\'Serial console ({"serial" if i == 0 else "console"}{i})\') button')
b.wait(lambda: m.execute(
f'ps aux | grep \'virsh -c qemu:///system console vmWithSerialConsole {"serial" if i == 0 else "console"}{i}\'')) # noqa: B023, E501
# disconnecting the serial console closes the pty channel
self.allow_journal_messages("connection unexpectedly closed by peer",
".*Connection reset by peer")
self.allow_browser_errors("Disconnection timed out.",
"Failed when connecting: Connection closed")
self.allow_journal_messages(".* couldn't shutdown fd: Transport endpoint is not connected")
self.allow_journal_messages("127.0.0.1:5900: couldn't read: Connection refused")
def testBasic(self):
b = self.browser
name = "subVmTest1"
self.createVm(name, graphics="vnc", ptyconsole=True)
self.login_and_go("/machines")
self.waitPageInit()
self.waitVmRow(name)
self.goToVmPage(name)
b.wait_in_text(f"#vm-{name}-system-state", "Running")
# test switching console from serial to graphical
b.wait_visible(f"#vm-{name}-consoles")
b.wait_visible(".pf-v5-c-console__vnc canvas")
b.click("#pf-v5-c-console__type-selector")
b.wait_visible("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.click("#SerialConsole button")
b.wait_not_present("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.wait_not_present(".pf-v5-c-console__vnc canvas")
b.wait_visible(f"#{name}-terminal")
# Go back to Vnc console
b.click("#pf-v5-c-console__type-selector")
b.wait_visible("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.click("#VncConsole button")
b.wait_not_present("#pf-v5-c-console__type-selector + .pf-v5-c-select__menu")
b.wait_visible(".pf-v5-c-console__vnc canvas")
# Go to the expanded console view
b.click("button:contains(Expand)")
# Test message is present if VM is not running
self.performAction(name, "forceOff", checkExpectedState=False)
b.wait_in_text("#vm-not-running-message", "start the virtual machine")
# Test deleting VM from console page will not trigger any error
self.performAction(name, "delete")
b.wait_visible(f"#vm-{name}-delete-modal-dialog")
b.click(f"#vm-{name}-delete-modal-dialog button:contains(Delete)")
self.waitPageInit()
self.waitVmRow(name, present=False)
b.wait_not_present("#navbar-oops")
self.allow_journal_messages("connection unexpectedly closed by peer")
self.allow_browser_errors("Disconnection timed out.",
"Failed when connecting: Connection closed")
if __name__ == '__main__':
testlib.test_main()