qemu-compose 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qemu_compose/__init__.py +0 -0
- qemu_compose/jsonlisp.py +254 -0
- qemu_compose/log_tool.py +64 -0
- qemu_compose/main.py +270 -0
- qemu_compose/qemu/__init__.py +3 -0
- qemu_compose/qemu/machine/__init__.py +36 -0
- qemu_compose/qemu/machine/console_socket.py +142 -0
- qemu_compose/qemu/machine/machine.py +958 -0
- qemu_compose/qemu/machine/py.typed +0 -0
- qemu_compose/qemu/machine/qtest.py +191 -0
- qemu_compose/qemu/qmp/__init__.py +59 -0
- qemu_compose/qemu/qmp/error.py +50 -0
- qemu_compose/qemu/qmp/events.py +717 -0
- qemu_compose/qemu/qmp/legacy.py +319 -0
- qemu_compose/qemu/qmp/message.py +209 -0
- qemu_compose/qemu/qmp/models.py +146 -0
- qemu_compose/qemu/qmp/protocol.py +1057 -0
- qemu_compose/qemu/qmp/py.typed +0 -0
- qemu_compose/qemu/qmp/qmp_client.py +655 -0
- qemu_compose/qemu/qmp/qmp_shell.py +618 -0
- qemu_compose/qemu/qmp/qmp_tui.py +655 -0
- qemu_compose/qemu/qmp/util.py +219 -0
- qemu_compose/qemu/utils/__init__.py +162 -0
- qemu_compose/qemu/utils/accel.py +84 -0
- qemu_compose/qemu/utils/py.typed +0 -0
- qemu_compose/qemu/utils/qemu_ga_client.py +323 -0
- qemu_compose/qemu/utils/qom.py +273 -0
- qemu_compose/qemu/utils/qom_common.py +175 -0
- qemu_compose/qemu/utils/qom_fuse.py +207 -0
- qemu_compose/zio.py +2165 -0
- qemu_compose-0.1.0.dist-info/METADATA +36 -0
- qemu_compose-0.1.0.dist-info/RECORD +35 -0
- qemu_compose-0.1.0.dist-info/WHEEL +4 -0
- qemu_compose-0.1.0.dist-info/entry_points.txt +2 -0
- qemu_compose-0.1.0.dist-info/licenses/LICENSE.txt +37 -0
qemu_compose/__init__.py
ADDED
|
File without changes
|
qemu_compose/jsonlisp.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
|
|
2
|
+
# NOTE: the following code comes from https://github.com/Leden/jsonlisp with small modification for this project
|
|
3
|
+
# but I have no idea why the original repo is 404 now, so I just leave a comment here to express thanks to Leden
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import operator
|
|
8
|
+
from collections import ChainMap
|
|
9
|
+
import ast
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
Symbol = str
|
|
14
|
+
List = list
|
|
15
|
+
Number = (int, float)
|
|
16
|
+
Dict = dict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Env(ChainMap):
|
|
20
|
+
def __json__(self):
|
|
21
|
+
return self.maps
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Proc:
|
|
25
|
+
def __init__(self, params, body, env):
|
|
26
|
+
self.params = params
|
|
27
|
+
self.body = body
|
|
28
|
+
self.env = env
|
|
29
|
+
|
|
30
|
+
def __call__(self, *args):
|
|
31
|
+
env = Env(dict(zip(self.params, args)), self.env)
|
|
32
|
+
return interp(self.body, env)
|
|
33
|
+
|
|
34
|
+
def __json__(self):
|
|
35
|
+
return ["lambda", self.params, self.body]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Macro:
|
|
39
|
+
def __init__(self, params, body, env):
|
|
40
|
+
self.params = params
|
|
41
|
+
self.body = body
|
|
42
|
+
self.env = env
|
|
43
|
+
|
|
44
|
+
def expand(self, *args, env):
|
|
45
|
+
static_env = Env(dict(zip(self.params, args)), self.env)
|
|
46
|
+
exp = interp(self.body, static_env)
|
|
47
|
+
return interp(exp, env)
|
|
48
|
+
|
|
49
|
+
def __json__(self):
|
|
50
|
+
return ["macro", self.params, self.body]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
builtins = {
|
|
54
|
+
# Operators
|
|
55
|
+
"*": operator.mul,
|
|
56
|
+
"+": operator.add,
|
|
57
|
+
"-": operator.sub,
|
|
58
|
+
"/": operator.truediv,
|
|
59
|
+
"<": operator.lt,
|
|
60
|
+
"<=": operator.le,
|
|
61
|
+
"=": operator.eq,
|
|
62
|
+
">": operator.gt,
|
|
63
|
+
">=": operator.ge,
|
|
64
|
+
"^": operator.xor,
|
|
65
|
+
"and": operator.and_,
|
|
66
|
+
"contains": operator.contains,
|
|
67
|
+
"in": lambda x, y: x in y,
|
|
68
|
+
"is": operator.is_,
|
|
69
|
+
"is-not": operator.is_not,
|
|
70
|
+
"not": operator.not_,
|
|
71
|
+
"or": operator.or_,
|
|
72
|
+
"xor": lambda x, y: bool(x) ^ bool(y), # logical XOR
|
|
73
|
+
# Typechecks
|
|
74
|
+
"dict?": lambda x: isinstance(x, Dict),
|
|
75
|
+
"list?": lambda x: isinstance(x, List),
|
|
76
|
+
"macro?": lambda x: isinstance(x, Macro),
|
|
77
|
+
"null?": lambda x: x is None,
|
|
78
|
+
"number?": lambda x: isinstance(x, Number),
|
|
79
|
+
"proc?": callable,
|
|
80
|
+
"symbol?": lambda x: isinstance(x, Symbol),
|
|
81
|
+
# List functions
|
|
82
|
+
"begin": lambda *x: x[-1],
|
|
83
|
+
"cons": lambda x, y: [x, *y],
|
|
84
|
+
"head": lambda x: x[0],
|
|
85
|
+
"len": len,
|
|
86
|
+
"list": lambda *x: list(x),
|
|
87
|
+
"map": lambda *args: list(map(*args)),
|
|
88
|
+
"range": lambda *x: list(range(*x)),
|
|
89
|
+
"tail": lambda x: x[1:],
|
|
90
|
+
# Dict functions
|
|
91
|
+
"dict": lambda x: dict(x),
|
|
92
|
+
"dict-del": lambda d, k: {_k: d[_k] for _k in d if k != _k},
|
|
93
|
+
"dict-get": lambda d, k: d.get(k),
|
|
94
|
+
"dict-items": lambda x: list(x.items()),
|
|
95
|
+
"dict-set": lambda d, k, v: {**d, k: v},
|
|
96
|
+
# Misc functions
|
|
97
|
+
"apply": lambda proc, args: proc(*args),
|
|
98
|
+
"print": print,
|
|
99
|
+
"literal": ast.literal_eval,
|
|
100
|
+
"str": str,
|
|
101
|
+
"format": lambda x, *args: x % tuple(args),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
std_lib = [
|
|
106
|
+
"list",
|
|
107
|
+
[
|
|
108
|
+
"def",
|
|
109
|
+
"defmacro",
|
|
110
|
+
[
|
|
111
|
+
"macro",
|
|
112
|
+
["name", "params", "body"],
|
|
113
|
+
[
|
|
114
|
+
"list",
|
|
115
|
+
["quote", "def"],
|
|
116
|
+
"name",
|
|
117
|
+
["list", ["quote", "macro"], "params", "body"],
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"defmacro",
|
|
123
|
+
"defproc",
|
|
124
|
+
["name", "params", "body"],
|
|
125
|
+
[
|
|
126
|
+
"list",
|
|
127
|
+
["quote", "def"],
|
|
128
|
+
"name",
|
|
129
|
+
["list", ["quote", "lambda"], "params", "body"],
|
|
130
|
+
],
|
|
131
|
+
],
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def default_env():
|
|
136
|
+
env = Env()
|
|
137
|
+
env.update(builtins)
|
|
138
|
+
env['key_up'] = "\x1b[A"
|
|
139
|
+
env['key_down'] = "\x1b[B"
|
|
140
|
+
env['key_right'] = "\x1b[C"
|
|
141
|
+
env['key_left'] = "\x1b[D"
|
|
142
|
+
env['key_home'] = "\x1b[H"
|
|
143
|
+
env['key_end'] = "\x1b[F"
|
|
144
|
+
env['key_ctrl_space'] = '\x00'
|
|
145
|
+
env['key_escape'] = '\x1b'
|
|
146
|
+
env['key_tab'] = '\t'
|
|
147
|
+
env['key_enter'] = '\n'
|
|
148
|
+
env['key_backspace'] = '\x7f'
|
|
149
|
+
interp(std_lib, env)
|
|
150
|
+
return env
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse(line):
|
|
154
|
+
return json.loads(line)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_file(path):
|
|
158
|
+
return json.load(path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def unparse(expr):
|
|
162
|
+
return json.dumps(expr, default=to_json)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def to_json(obj):
|
|
166
|
+
if hasattr(obj, "__json__"):
|
|
167
|
+
return obj.__json__()
|
|
168
|
+
return repr(obj)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def interp(x, env):
|
|
172
|
+
# a shortcut syntax for human readability
|
|
173
|
+
if isinstance(x, Dict) and len(x) == 1:
|
|
174
|
+
for k in x:
|
|
175
|
+
v = x[k]
|
|
176
|
+
if isinstance(v, List) or isinstance(v, Dict):
|
|
177
|
+
v = interp(v, env)
|
|
178
|
+
|
|
179
|
+
if not isinstance(v, List):
|
|
180
|
+
v = [v]
|
|
181
|
+
|
|
182
|
+
f = env[k]
|
|
183
|
+
logger.info('CALL_STEP: %s %s' % (f.__name__, v))
|
|
184
|
+
return f(*v)
|
|
185
|
+
|
|
186
|
+
if isinstance(x, Symbol):
|
|
187
|
+
if x.startswith('key_') and len(x) == 5:
|
|
188
|
+
return x[4]
|
|
189
|
+
return env[x]
|
|
190
|
+
|
|
191
|
+
if not isinstance(x, List):
|
|
192
|
+
return x
|
|
193
|
+
|
|
194
|
+
if x == []:
|
|
195
|
+
return x
|
|
196
|
+
|
|
197
|
+
if x[0] == "quote" or x[0] == "'":
|
|
198
|
+
_, exp = x
|
|
199
|
+
return exp
|
|
200
|
+
|
|
201
|
+
if x[0] == "flat_quote" or x[0] == "_'":
|
|
202
|
+
_, *exp = x
|
|
203
|
+
return exp
|
|
204
|
+
|
|
205
|
+
if x[0] == "if":
|
|
206
|
+
_, test, conseq, alt = x
|
|
207
|
+
exp = conseq if interp(test, env) else alt
|
|
208
|
+
return interp(exp, env)
|
|
209
|
+
|
|
210
|
+
if x[0] == "def":
|
|
211
|
+
_, var, exp = x
|
|
212
|
+
val = env[var] = interp(exp, env)
|
|
213
|
+
return val
|
|
214
|
+
|
|
215
|
+
if x[0] == "lambda":
|
|
216
|
+
_, params, body = x
|
|
217
|
+
return Proc(params, body, env)
|
|
218
|
+
|
|
219
|
+
if x[0] == "macro":
|
|
220
|
+
_, params, body = x
|
|
221
|
+
return Macro(params, body, env)
|
|
222
|
+
|
|
223
|
+
proc_or_macro_exp, *args = x
|
|
224
|
+
proc = interp(proc_or_macro_exp, env)
|
|
225
|
+
if isinstance(proc, Macro):
|
|
226
|
+
return proc.expand(*args, env=env)
|
|
227
|
+
else:
|
|
228
|
+
args = [interp(exp, env) for exp in args]
|
|
229
|
+
if callable(proc):
|
|
230
|
+
if proc_or_macro_exp != "begin":
|
|
231
|
+
logger.info('CALL_STEP: %s(%s) %s' % (proc.__name__, proc_or_macro_exp, args))
|
|
232
|
+
else:
|
|
233
|
+
logger.error('CALL_STEP:ERROR: function not callable: %s(%s) %s' % (proc, proc_or_macro_exp, args))
|
|
234
|
+
return proc(*args)
|
|
235
|
+
|
|
236
|
+
def repl(prompt=r"{λ}> "):
|
|
237
|
+
env = default_env()
|
|
238
|
+
|
|
239
|
+
while True:
|
|
240
|
+
try:
|
|
241
|
+
line = input(prompt)
|
|
242
|
+
print(unparse(interp(parse(line), env)))
|
|
243
|
+
except EOFError:
|
|
244
|
+
print("Bye")
|
|
245
|
+
break
|
|
246
|
+
except Exception as err:
|
|
247
|
+
print(repr(err))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def run_file(path):
|
|
251
|
+
env = default_env()
|
|
252
|
+
|
|
253
|
+
with open(path) as f:
|
|
254
|
+
print(unparse(interp(parse_file(f), env)))
|
qemu_compose/log_tool.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
import socket
|
|
6
|
+
from zio import zio
|
|
7
|
+
|
|
8
|
+
def parse(log_path:str):
|
|
9
|
+
buf = [bytearray(), bytearray()]
|
|
10
|
+
ret = []
|
|
11
|
+
|
|
12
|
+
for line in open(log_path):
|
|
13
|
+
if 'IO.recv(' in line:
|
|
14
|
+
if buf[1]:
|
|
15
|
+
ret.append((1, bytes(buf[1])))
|
|
16
|
+
buf[1] = bytearray()
|
|
17
|
+
i = line.index(' -> ')
|
|
18
|
+
content = line[i+4:-1].strip()
|
|
19
|
+
assert (content.startswith('b"') and content.endswith('"')) or (content.startswith("b'") and content.endswith("'"))
|
|
20
|
+
buf[0].extend(ast.literal_eval(content))
|
|
21
|
+
elif 'IO.send(' in line:
|
|
22
|
+
if buf[0]:
|
|
23
|
+
ret.append((0, bytes(buf[0])))
|
|
24
|
+
buf[0] = bytearray()
|
|
25
|
+
i = line.index(' -> ')
|
|
26
|
+
content = line[i+4:-1].strip()
|
|
27
|
+
assert (content.startswith('b"') and content.endswith('"')) or (content.startswith("b'") and content.endswith("'"))
|
|
28
|
+
buf[1].extend(ast.literal_eval(content))
|
|
29
|
+
else:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
if buf[0]:
|
|
33
|
+
ret.append((0, bytes(buf[0])))
|
|
34
|
+
if buf[1]:
|
|
35
|
+
ret.append((1, bytes(buf[1])))
|
|
36
|
+
return ret
|
|
37
|
+
|
|
38
|
+
def serve(logs, addr=None):
|
|
39
|
+
server = socket.socket()
|
|
40
|
+
if addr is None:
|
|
41
|
+
addr = ('0.0.0.0', 1111)
|
|
42
|
+
server.bind(addr)
|
|
43
|
+
server.listen(1)
|
|
44
|
+
s, _ = server.accept()
|
|
45
|
+
io = zio(s, print_read=False, print_write=False, timeout=3600)
|
|
46
|
+
for d, c in logs:
|
|
47
|
+
if d == 1:
|
|
48
|
+
print('EXPECT', c)
|
|
49
|
+
io.read_until(c)
|
|
50
|
+
else:
|
|
51
|
+
print('SEND', c)
|
|
52
|
+
io.write(c)
|
|
53
|
+
io.close()
|
|
54
|
+
|
|
55
|
+
def main(name, log_path):
|
|
56
|
+
logs = parse(log_path)
|
|
57
|
+
if name == 'parse':
|
|
58
|
+
for direction, content in logs:
|
|
59
|
+
print('<<<<----' if direction else '---->>>>', content)
|
|
60
|
+
elif name == 'serve':
|
|
61
|
+
serve(logs)
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
main(sys.argv[1], sys.argv[2])
|
qemu_compose/main.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import yaml
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
import threading
|
|
10
|
+
import tty
|
|
11
|
+
import subprocess
|
|
12
|
+
from functools import partial
|
|
13
|
+
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
|
|
14
|
+
|
|
15
|
+
from .qemu.machine import QEMUMachine
|
|
16
|
+
from .qemu.machine.machine import AbnormalShutdown
|
|
17
|
+
from .jsonlisp import default_env, interp
|
|
18
|
+
|
|
19
|
+
from .zio import zio, write_debug, select_ignoring_useless_signal, ttyraw
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("resident")
|
|
22
|
+
|
|
23
|
+
class HttpServer:
|
|
24
|
+
def __init__(self, listen:str, port:int, root:str):
|
|
25
|
+
self.listen = listen
|
|
26
|
+
self.port = port
|
|
27
|
+
self.root = root
|
|
28
|
+
|
|
29
|
+
def start(self):
|
|
30
|
+
http_handler = partial(SimpleHTTPRequestHandler, directory=self.root)
|
|
31
|
+
server = ThreadingHTTPServer((self.listen, self.port), http_handler)
|
|
32
|
+
server_thread = threading.Thread(target=server.serve_forever)
|
|
33
|
+
server_thread.daemon = True
|
|
34
|
+
server_thread.start()
|
|
35
|
+
|
|
36
|
+
class StreamWrapper:
|
|
37
|
+
def __init__(self, obj):
|
|
38
|
+
self.obj = obj
|
|
39
|
+
|
|
40
|
+
def __getattr__(self, name):
|
|
41
|
+
return getattr(self.obj, name)
|
|
42
|
+
|
|
43
|
+
def write(self, s):
|
|
44
|
+
if isinstance(s, str):
|
|
45
|
+
s = s.encode()
|
|
46
|
+
self.obj.write(s)
|
|
47
|
+
|
|
48
|
+
class Terminal(object):
|
|
49
|
+
def __init__(self, fd, log_path=None):
|
|
50
|
+
self.fd = fd
|
|
51
|
+
|
|
52
|
+
if isinstance(log_path, str):
|
|
53
|
+
self.debug_file = open(log_path, "wb") if log_path else None
|
|
54
|
+
else:
|
|
55
|
+
self.debug_file = log_path
|
|
56
|
+
|
|
57
|
+
self.io = zio(fd, print_write=False, logfile=sys.stdout, debug=self.debug_file, timeout=3600)
|
|
58
|
+
|
|
59
|
+
self.term_feed_running = False
|
|
60
|
+
self.term_feed_drain_thread = None
|
|
61
|
+
|
|
62
|
+
if not os.isatty(0):
|
|
63
|
+
raise Exception('resident.Terminal must run in a UNIX 98 style pty/tty')
|
|
64
|
+
|
|
65
|
+
def term_feed_loop(self):
|
|
66
|
+
logger.info('Terminal.term_feed_loop started...')
|
|
67
|
+
while self.term_feed_running:
|
|
68
|
+
r, _, _ = select_ignoring_useless_signal([0], [], [], 0.2)
|
|
69
|
+
|
|
70
|
+
if 0 in r:
|
|
71
|
+
data = os.read(0, 1024)
|
|
72
|
+
if data:
|
|
73
|
+
logger.info('Terminal.term_feed_loop received(%d) -> %s' % (len(data), data))
|
|
74
|
+
self.io.write(data)
|
|
75
|
+
|
|
76
|
+
logger.info('Terminal.term_feed_loop finished.')
|
|
77
|
+
|
|
78
|
+
def run_batch(self, cmds:List, env_variables=None):
|
|
79
|
+
if not isinstance(cmds, list):
|
|
80
|
+
raise ValueError("cmds must be a list")
|
|
81
|
+
|
|
82
|
+
current_tty_mode = tty.tcgetattr(0)[:]
|
|
83
|
+
ttyraw(0)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
self.term_feed_running = True
|
|
87
|
+
self.term_feed_drain_thread = threading.Thread(target=self.term_feed_loop)
|
|
88
|
+
self.term_feed_drain_thread.daemon = True
|
|
89
|
+
self.term_feed_drain_thread.start()
|
|
90
|
+
|
|
91
|
+
io = self.io
|
|
92
|
+
|
|
93
|
+
if self.debug_file:
|
|
94
|
+
write_debug(self.debug_file, b'run_batch: cmds = %r' % cmds)
|
|
95
|
+
|
|
96
|
+
transpiled_cmds = ['begin'] + cmds
|
|
97
|
+
|
|
98
|
+
env = default_env()
|
|
99
|
+
|
|
100
|
+
env['read_until'] = io.read_until
|
|
101
|
+
env['write'] = io.write
|
|
102
|
+
env['writeline'] = io.writeline
|
|
103
|
+
env['wait'] = io.read_until_timeout
|
|
104
|
+
env['RegExp'] = lambda x: re.compile(x.encode())
|
|
105
|
+
env['interact'] = self.interact
|
|
106
|
+
|
|
107
|
+
if env_variables:
|
|
108
|
+
env.update(env_variables)
|
|
109
|
+
|
|
110
|
+
interp(transpiled_cmds, env)
|
|
111
|
+
|
|
112
|
+
finally:
|
|
113
|
+
tty.tcsetattr(0, tty.TCSAFLUSH, current_tty_mode)
|
|
114
|
+
|
|
115
|
+
def interact(self, buffered:Optional[bytes]=None, raw_mode=False):
|
|
116
|
+
|
|
117
|
+
self.term_feed_running = False
|
|
118
|
+
if self.term_feed_drain_thread is not None:
|
|
119
|
+
self.term_feed_drain_thread.join()
|
|
120
|
+
|
|
121
|
+
self.io.interactive(raw_mode=raw_mode, buffered=buffered)
|
|
122
|
+
|
|
123
|
+
def extract_format_or_default(mapping:dict, key:str, env:dict, default=None):
|
|
124
|
+
value = mapping.get(key) if mapping else key
|
|
125
|
+
if value:
|
|
126
|
+
# FIXME: format has security issues
|
|
127
|
+
return str(value).format(**env)
|
|
128
|
+
return default
|
|
129
|
+
|
|
130
|
+
def run(config_path, log_path=None, env_update=None):
|
|
131
|
+
|
|
132
|
+
if log_path:
|
|
133
|
+
debug_file = open(log_path, "wb")
|
|
134
|
+
logging.basicConfig(level=logging.DEBUG, stream=StreamWrapper(debug_file))
|
|
135
|
+
else:
|
|
136
|
+
debug_file = None
|
|
137
|
+
|
|
138
|
+
config: dict
|
|
139
|
+
with open(config_path) as f:
|
|
140
|
+
config = yaml.safe_load(f)
|
|
141
|
+
|
|
142
|
+
name = config.get('name')
|
|
143
|
+
|
|
144
|
+
binary = config.get('binary', shutil.which('qemu-system-x86_64'))
|
|
145
|
+
if not binary:
|
|
146
|
+
raise Exception('qemu binary not found')
|
|
147
|
+
|
|
148
|
+
cwd = os.path.normpath(os.path.dirname(config_path))
|
|
149
|
+
term_size = os.get_terminal_size()
|
|
150
|
+
|
|
151
|
+
env = {
|
|
152
|
+
'CWD': cwd,
|
|
153
|
+
'GATEWAY_IP': '10.0.2.2', # qemu user network default gateway ip
|
|
154
|
+
'TERM_ROWS': term_size.lines,
|
|
155
|
+
'TERM_COLS': term_size.columns,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if config.get('env'):
|
|
159
|
+
for k in config.get('env'):
|
|
160
|
+
env[k] = config.get('env')[k]
|
|
161
|
+
|
|
162
|
+
os.chdir(env['CWD'])
|
|
163
|
+
|
|
164
|
+
http_port = None
|
|
165
|
+
if config.get('http_serve'):
|
|
166
|
+
http_serve_config:dict = config.get('http_serve')
|
|
167
|
+
http_listen = extract_format_or_default(http_serve_config, 'listen', env, default='0.0.0.0')
|
|
168
|
+
http_port = int(extract_format_or_default(http_serve_config, 'port', env, default=8888))
|
|
169
|
+
http_root = extract_format_or_default(http_serve_config, 'root', env, default=env['CWD'])
|
|
170
|
+
|
|
171
|
+
http_server = HttpServer(http_listen, http_port, http_root)
|
|
172
|
+
http_server.start()
|
|
173
|
+
logger.info('HTTP server started on %s:%d, serving %s' % (http_listen, http_port, http_root))
|
|
174
|
+
|
|
175
|
+
if http_port is not None:
|
|
176
|
+
env['HTTP_PORT'] = http_port
|
|
177
|
+
access_ip = extract_format_or_default(config.get('http_serve'), 'access_ip', env, default=env['GATEWAY_IP']) if config.get('http_serve') else env['GATEWAY_IP']
|
|
178
|
+
env['HTTP_HOST'] = access_ip
|
|
179
|
+
|
|
180
|
+
if env_update:
|
|
181
|
+
env.update(env_update)
|
|
182
|
+
|
|
183
|
+
if config.get('before_script'):
|
|
184
|
+
for line in config.get('before_script'):
|
|
185
|
+
command = extract_format_or_default(None, line, env)
|
|
186
|
+
if command:
|
|
187
|
+
subprocess.run(command.strip(), shell=True, check=True)
|
|
188
|
+
|
|
189
|
+
default_args = {
|
|
190
|
+
'cpu': 'max',
|
|
191
|
+
'machine': 'type=q35',
|
|
192
|
+
'accel': 'kvm',
|
|
193
|
+
'm': '1G',
|
|
194
|
+
'smp': '1',
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for block in config.get('args'):
|
|
198
|
+
for key in block:
|
|
199
|
+
val = extract_format_or_default(block, key, env)
|
|
200
|
+
if key in default_args:
|
|
201
|
+
default_args[key] = val
|
|
202
|
+
|
|
203
|
+
args = []
|
|
204
|
+
for key in default_args:
|
|
205
|
+
args.append('-' + key)
|
|
206
|
+
args.append(default_args[key])
|
|
207
|
+
|
|
208
|
+
for block in config.get('args'):
|
|
209
|
+
for key in block:
|
|
210
|
+
val = extract_format_or_default(block, key, env)
|
|
211
|
+
if key in default_args:
|
|
212
|
+
continue
|
|
213
|
+
args.append('-' + key)
|
|
214
|
+
if val is not None:
|
|
215
|
+
args.append(val)
|
|
216
|
+
|
|
217
|
+
vm = QEMUMachine(binary, args=args, name=name)
|
|
218
|
+
vm.add_monitor_null()
|
|
219
|
+
vm.set_qmp_monitor(True)
|
|
220
|
+
vm.set_console(console_chardev='socket', device_type='isa-serial')
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
vm.launch()
|
|
224
|
+
|
|
225
|
+
term = Terminal(vm.console_file, debug_file)
|
|
226
|
+
|
|
227
|
+
boot_commands = config.get('boot_commands')
|
|
228
|
+
if boot_commands:
|
|
229
|
+
term.run_batch(boot_commands, env_variables=env)
|
|
230
|
+
else:
|
|
231
|
+
term.interact(raw_mode=True)
|
|
232
|
+
|
|
233
|
+
if config.get('after_script'):
|
|
234
|
+
for line in config.get('after_script'):
|
|
235
|
+
command = extract_format_or_default(None, line, env)
|
|
236
|
+
if command:
|
|
237
|
+
subprocess.run(command.strip(), shell=True, check=True)
|
|
238
|
+
|
|
239
|
+
except KeyboardInterrupt:
|
|
240
|
+
logger.warning("Keyboard interrupt, shutting down vm...")
|
|
241
|
+
finally:
|
|
242
|
+
try:
|
|
243
|
+
if vm.is_running():
|
|
244
|
+
vm.shutdown(hard=True)
|
|
245
|
+
except AbnormalShutdown:
|
|
246
|
+
logger.error('abnormal shutdown exception')
|
|
247
|
+
finally:
|
|
248
|
+
vm._load_io_log()
|
|
249
|
+
logger.info('vm.process_io_log = %r' % (vm.get_log(), ))
|
|
250
|
+
|
|
251
|
+
def guess_conf_path(p:str | None):
|
|
252
|
+
if p:
|
|
253
|
+
return p
|
|
254
|
+
for f in ["qemu-compose.yml", "qemu-compose.yaml"]:
|
|
255
|
+
if os.path.exists(f):
|
|
256
|
+
return f
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def cli():
|
|
260
|
+
import argparse
|
|
261
|
+
parser = argparse.ArgumentParser(description="Define and run QEMU VM with qemu")
|
|
262
|
+
parser.add_argument('command', type=str, help='command to run')
|
|
263
|
+
parser.add_argument('-f', "--file", type=str, help='Compose configuration files')
|
|
264
|
+
args = parser.parse_args()
|
|
265
|
+
if args.command == "up":
|
|
266
|
+
conf_path = guess_conf_path(args.file)
|
|
267
|
+
if not conf_path:
|
|
268
|
+
print("qemu-compose.yml not found", file=sys.stderr)
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
run(conf_path)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QEMU development and testing library.
|
|
3
|
+
|
|
4
|
+
This library provides a few high-level classes for driving QEMU from a
|
|
5
|
+
test suite, not intended for production use.
|
|
6
|
+
|
|
7
|
+
| QEMUQtestProtocol: send/receive qtest messages.
|
|
8
|
+
| QEMUMachine: Configure and Boot a QEMU VM
|
|
9
|
+
| +-- QEMUQtestMachine: VM class, with a qtest socket.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Copyright (C) 2020-2021 John Snow for Red Hat Inc.
|
|
14
|
+
# Copyright (C) 2015-2016 Red Hat Inc.
|
|
15
|
+
# Copyright (C) 2012 IBM Corp.
|
|
16
|
+
#
|
|
17
|
+
# Authors:
|
|
18
|
+
# John Snow <jsnow@redhat.com>
|
|
19
|
+
# Fam Zheng <fam@euphon.net>
|
|
20
|
+
#
|
|
21
|
+
# This work is licensed under the terms of the GNU GPL, version 2. See
|
|
22
|
+
# the COPYING file in the top-level directory.
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
# pylint: disable=import-error
|
|
26
|
+
# see: https://github.com/PyCQA/pylint/issues/3624
|
|
27
|
+
# see: https://github.com/PyCQA/pylint/issues/3651
|
|
28
|
+
from .machine import QEMUMachine
|
|
29
|
+
from .qtest import QEMUQtestMachine, QEMUQtestProtocol
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = (
|
|
33
|
+
'QEMUMachine',
|
|
34
|
+
'QEMUQtestProtocol',
|
|
35
|
+
'QEMUQtestMachine',
|
|
36
|
+
)
|