-
Notifications
You must be signed in to change notification settings - Fork 364
/
serialconsole.py
379 lines (302 loc) · 11.3 KB
/
serialconsole.py
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#
# Project Kimchi
#
# Copyright IBM Corp, 2016
#
# This library 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 library 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 library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
import os
import socket
import sys
import threading
import time
from multiprocessing import Process
import libvirt
from wok.config import config as wok_config
from wok.plugins.kimchi import model
from wok.utils import wok_log
SOCKET_QUEUE_BACKLOG = 0
CTRL_Q = '\x11'
BASE_DIRECTORY = '/run'
class SocketServer(Process):
"""Unix socket server for guest console access.
Implements a unix socket server for each guest, this server will receive
data from a particular client, forward that data to the guest console,
receive the response from the console and send the response back to the
client.
Features:
- one socket server per client connection;
- server listens to unix socket;
- exclusive connection per guest;
- websockity handles the proxy between the client websocket to the
local unix socket;
Note:
- old versions (< 0.6.0)of websockify don't handle their children
processes accordingly, leaving a zombie process behind (this also
happens with novnc).
"""
def __init__(self, guest_name, URI):
"""Constructs a unix socket server.
Listens to connections on /run/<guest name>.
"""
Process.__init__(self)
self._guest_name = guest_name
self._uri = URI
self._server_addr = os.path.join(BASE_DIRECTORY, guest_name)
if os.path.exists(self._server_addr):
raise RuntimeError(
'There is an existing connection to %s' % guest_name)
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket.bind(self._server_addr)
self._socket.listen(SOCKET_QUEUE_BACKLOG)
wok_log.info('[%s] socket server to guest %s created',
self.name, guest_name)
def run(self):
"""Implements customized run method from Process.
"""
self.listen()
def _is_vm_listening_serial(self, console):
"""Checks if the guest is listening (reading/writing) to the serial
console.
"""
is_listening = []
def _test_output(stream, event, opaque):
is_listening.append(1)
def _event_loop():
while not is_listening:
libvirt.virEventRunDefaultImpl()
console.eventAddCallback(
libvirt.VIR_STREAM_EVENT_READABLE, _test_output, None)
libvirt_loop = threading.Thread(target=_event_loop)
libvirt_loop.start()
console.send(b'\n')
libvirt_loop.join(1)
if not libvirt_loop.is_alive():
console.eventRemoveCallback()
return True
console.eventRemoveCallback()
return False
def _send_to_client(self, stream, event, opaque):
"""Handles libvirt stream readable events.
Each event will be send back to the client socket.
"""
try:
data = stream.recv(1024)
except Exception as e:
wok_log.info(
'[%s] Error when reading from console: %s', self.name, str(e))
return
# return if no data received or client socket(opaque) is not valid
if not data or not opaque:
return
opaque.send(data)
def libvirt_event_loop(self, guest, client):
"""Runs libvirt event loop.
"""
# stop the event loop when the guest is not running
while guest.is_running():
libvirt.virEventRunDefaultImpl()
# shutdown the client socket to unblock the recv and stop the
# server as soon as the guest shuts down
client.shutdown(socket.SHUT_RD)
def listen(self):
"""Prepares the environment before starts to accept connections
Initializes and destroy the resources needed to accept connection.
"""
libvirt.virEventRegisterDefaultImpl()
try:
guest = LibvirtGuest(self._guest_name, self._uri, self.name)
except Exception as e:
wok_log.error(
'[%s] Cannot open the guest %s due to %s',
self.name,
self._guest_name,
str(e),
)
self._socket.close()
sys.exit(1)
except (KeyboardInterrupt, SystemExit):
self._socket.close()
sys.exit(1)
console = None
try:
console = guest.get_console()
if console is None:
wok_log.error(
'[%s] Cannot get the console to %s', self.name, self._guest_name
)
return
if not self._is_vm_listening_serial(console):
sys.exit(1)
self._listen(guest, console)
# clear resources aquired when the process is killed
except (KeyboardInterrupt, SystemExit):
pass
finally:
wok_log.info(
'[%s] Shutting down the socket server to %s console',
self.name,
self._guest_name,
)
self._socket.close()
if os.path.exists(self._server_addr):
os.unlink(self._server_addr)
try:
console.eventRemoveCallback()
except Exception as e:
wok_log.info(
'[%s] Callback is probably removed: %s', self.name, str(e))
guest.close()
def _listen(self, guest, console):
"""Accepts client connections.
Each connection is directly linked to the desired guest console. Thus
any data received from the client can be send to the guest console as
well as any response from the guest console can be send back to the
client console.
"""
client, client_addr = self._socket.accept()
session_timeout = wok_config.get('server', 'session_timeout')
client.settimeout(int(session_timeout) * 60)
wok_log.info('[%s] Client connected to %s',
self.name, self._guest_name)
# register the callback to receive any data from the console
console.eventAddCallback(
libvirt.VIR_STREAM_EVENT_READABLE, self._send_to_client, client
)
# start the libvirt event loop in a python thread
libvirt_loop = threading.Thread(
target=self.libvirt_event_loop, args=(guest, client)
)
libvirt_loop.start()
while True:
data = ''
try:
data = client.recv(1024)
except Exception as e:
wok_log.info(
'[%s] Client disconnected from %s: %s',
self.name,
self._guest_name,
str(e),
)
break
if not data or data == CTRL_Q:
break
# if the console can no longer be accessed, close everything
# and quits
try:
console.send(data)
except Exception:
wok_log.info(
'[%s] Console of %s is not accessible', self.name, self._guest_name
)
break
# clear used resources when the connection is closed and, if possible,
# tell the client the connection was lost.
try:
client.send(b'\\r\\n\\r\\nClient disconnected\\r\\n')
except Exception:
pass
# socket_server
class LibvirtGuest(object):
def __init__(self, guest_name, uri, process_name):
"""
Constructs a guest object that opens a connection to libvirt and
searchs for a particular guest, provided by the caller.
"""
self._proc_name = process_name
try:
libvirt = model.libvirtconnection.LibvirtConnection(uri)
self._guest = model.vms.VMModel.get_vm(guest_name, libvirt)
except Exception as e:
wok_log.error(
'[%s] Cannot open guest %s: %s', self._proc_name, guest_name, str(
e)
)
raise
self._libvirt = libvirt.get()
self._name = guest_name
self._stream = None
def is_running(self):
"""
Checks if this guest is currently in a running state.
"""
return (
self._guest.state(0)[0] == libvirt.VIR_DOMAIN_RUNNING or
self._guest.state(0)[0] == libvirt.VIR_DOMAIN_PAUSED
)
def get_console(self):
"""
Opens a console to this guest and returns a reference to it.
Note: If another instance (eg: virsh) has an existing console opened
to this guest, this code will steal that console.
"""
# guest must be in a running state to get its console
counter = 10
while not self.is_running():
wok_log.info(
'[%s] Guest %s is not running, waiting for it',
self._proc_name,
self._name,
)
counter -= 1
if counter <= 0:
return None
time.sleep(1)
# attach a stream in the guest console so we can read from/write to it
if self._stream is None:
wok_log.info(
'[%s] Opening the console for guest %s', self._proc_name, self._name
)
self._stream = self._libvirt.newStream(libvirt.VIR_STREAM_NONBLOCK)
self._guest.openConsole(
None,
self._stream,
libvirt.VIR_DOMAIN_CONSOLE_FORCE | libvirt.VIR_DOMAIN_CONSOLE_SAFE,
)
return self._stream
def close(self):
"""Closes the libvirt connection.
"""
self._libvirt.close()
# guest
def main(guest_name, URI='qemu:///system'):
"""Main entry point to create a socket server.
Starts a new socket server to listen messages to/from the guest.
"""
server = None
try:
server = SocketServer(guest_name, URI)
except Exception as e:
wok_log.error('Cannot create the socket server: %s', str(e))
raise
server.start()
return server
if __name__ == '__main__':
"""Executes a stand alone instance of the socket server.
This may be useful for testing/debugging.
In order to debug, add the path before importing kimchi/wok code:
sys.path.append('../../../')
start the server:
python serialconsole.py <guest_name>
and, on another terminal, run:
netcat -U /run/<guest_name>
"""
argc = len(sys.argv)
if argc != 2:
print(f'usage: ./{sys.argv[0]} <guest_name>')
sys.exit(1)
main(sys.argv[1])