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.
File without changes
@@ -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)))
@@ -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,3 @@
1
+
2
+ # code under this package comes from https://gitlab.com/qemu-project/qemu/-/tree/master/python/qemu with small modification
3
+ # they are licensed under https://gitlab.com/qemu-project/qemu/-/blob/master/LICENSE
@@ -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
+ )