qemu-compose 0.1.0__tar.gz

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.
Files changed (35) hide show
  1. qemu_compose-0.1.0/.gitignore +2 -0
  2. qemu_compose-0.1.0/LICENSE.txt +37 -0
  3. qemu_compose-0.1.0/PKG-INFO +36 -0
  4. qemu_compose-0.1.0/README.md +27 -0
  5. qemu_compose-0.1.0/pyproject.toml +34 -0
  6. qemu_compose-0.1.0/qemu_compose/__init__.py +0 -0
  7. qemu_compose-0.1.0/qemu_compose/jsonlisp.py +254 -0
  8. qemu_compose-0.1.0/qemu_compose/log_tool.py +64 -0
  9. qemu_compose-0.1.0/qemu_compose/main.py +270 -0
  10. qemu_compose-0.1.0/qemu_compose/qemu/__init__.py +3 -0
  11. qemu_compose-0.1.0/qemu_compose/qemu/machine/__init__.py +36 -0
  12. qemu_compose-0.1.0/qemu_compose/qemu/machine/console_socket.py +142 -0
  13. qemu_compose-0.1.0/qemu_compose/qemu/machine/machine.py +958 -0
  14. qemu_compose-0.1.0/qemu_compose/qemu/machine/py.typed +0 -0
  15. qemu_compose-0.1.0/qemu_compose/qemu/machine/qtest.py +191 -0
  16. qemu_compose-0.1.0/qemu_compose/qemu/qmp/__init__.py +59 -0
  17. qemu_compose-0.1.0/qemu_compose/qemu/qmp/error.py +50 -0
  18. qemu_compose-0.1.0/qemu_compose/qemu/qmp/events.py +717 -0
  19. qemu_compose-0.1.0/qemu_compose/qemu/qmp/legacy.py +319 -0
  20. qemu_compose-0.1.0/qemu_compose/qemu/qmp/message.py +209 -0
  21. qemu_compose-0.1.0/qemu_compose/qemu/qmp/models.py +146 -0
  22. qemu_compose-0.1.0/qemu_compose/qemu/qmp/protocol.py +1057 -0
  23. qemu_compose-0.1.0/qemu_compose/qemu/qmp/py.typed +0 -0
  24. qemu_compose-0.1.0/qemu_compose/qemu/qmp/qmp_client.py +655 -0
  25. qemu_compose-0.1.0/qemu_compose/qemu/qmp/qmp_shell.py +618 -0
  26. qemu_compose-0.1.0/qemu_compose/qemu/qmp/qmp_tui.py +655 -0
  27. qemu_compose-0.1.0/qemu_compose/qemu/qmp/util.py +219 -0
  28. qemu_compose-0.1.0/qemu_compose/qemu/utils/__init__.py +162 -0
  29. qemu_compose-0.1.0/qemu_compose/qemu/utils/accel.py +84 -0
  30. qemu_compose-0.1.0/qemu_compose/qemu/utils/py.typed +0 -0
  31. qemu_compose-0.1.0/qemu_compose/qemu/utils/qemu_ga_client.py +323 -0
  32. qemu_compose-0.1.0/qemu_compose/qemu/utils/qom.py +273 -0
  33. qemu_compose-0.1.0/qemu_compose/qemu/utils/qom_common.py +175 -0
  34. qemu_compose-0.1.0/qemu_compose/qemu/utils/qom_fuse.py +207 -0
  35. qemu_compose-0.1.0/qemu_compose/zio.py +2165 -0
@@ -0,0 +1,2 @@
1
+ **/__pycache__
2
+ **/*.pyc
@@ -0,0 +1,37 @@
1
+ The Star And Thank Author License (SATA)
2
+
3
+ Copyright (c) 2025 zTrix(i@ztrix.me)
4
+
5
+ Project Url: https://github.com/zTrix/qemu-compose
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ And wait, the most important, you shall star/+1/like the project(s) in project url
18
+ section above first, and then thank the author(s) in Copyright section.
19
+
20
+ Here are some suggested ways:
21
+
22
+ - Email the authors a thank-you letter, and make friends with him/her/them.
23
+ - Report bugs or issues.
24
+ - Tell friends what a wonderful project this is.
25
+ - And, sure, you can just express thanks in your mind without telling the world.
26
+
27
+ Contributors of this project by forking have the option to add his/her name and
28
+ forked project url at copyright and project url sections, but shall not delete
29
+ or modify anything else in these two sections.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
37
+ THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: qemu-compose
3
+ Version: 0.1.0
4
+ Summary: docker-compose style composer for qemu
5
+ License-File: LICENSE.txt
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: pyyaml>=6.0.2
8
+ Description-Content-Type: text/markdown
9
+
10
+
11
+ # qemu-compose
12
+
13
+ qemu-compose aims to provide a docker-compose style composer for qemu command, with advanced scripting feature as well as http support for cloud init or general purpose provisioning.
14
+
15
+ bring up a qemu VM by providing a qemu-compose.yml and run `qemu-compose up`
16
+
17
+ ## Advantages
18
+
19
+ - Simple and robust
20
+ - No libvirt stuff, no complex
21
+ - support `before_script` and `after_script` for setup and cleanup
22
+ - support `boot_commands` for vm provisioning (implemented using jsonlisp for expressive power which apparently is turing-complete)
23
+ - support `http_serve` for cloudinit
24
+ - env interpolation for advanced configuration
25
+
26
+ ## Examples and Screenshot
27
+
28
+ bring up ubuntu cloudimg qemu vm and run a interactive shell
29
+
30
+
31
+ ```
32
+ $ cd ./script/ubuntu-cloudimg__amd64/
33
+ $ qemu-compose up
34
+ ```
35
+
36
+ ![svg](https://raw.githubusercontent.com/zTrix/qemu-compose/refs/heads/main/assets/ubuntu-cloudimg.svg)
@@ -0,0 +1,27 @@
1
+
2
+ # qemu-compose
3
+
4
+ qemu-compose aims to provide a docker-compose style composer for qemu command, with advanced scripting feature as well as http support for cloud init or general purpose provisioning.
5
+
6
+ bring up a qemu VM by providing a qemu-compose.yml and run `qemu-compose up`
7
+
8
+ ## Advantages
9
+
10
+ - Simple and robust
11
+ - No libvirt stuff, no complex
12
+ - support `before_script` and `after_script` for setup and cleanup
13
+ - support `boot_commands` for vm provisioning (implemented using jsonlisp for expressive power which apparently is turing-complete)
14
+ - support `http_serve` for cloudinit
15
+ - env interpolation for advanced configuration
16
+
17
+ ## Examples and Screenshot
18
+
19
+ bring up ubuntu cloudimg qemu vm and run a interactive shell
20
+
21
+
22
+ ```
23
+ $ cd ./script/ubuntu-cloudimg__amd64/
24
+ $ qemu-compose up
25
+ ```
26
+
27
+ ![svg](https://raw.githubusercontent.com/zTrix/qemu-compose/refs/heads/main/assets/ubuntu-cloudimg.svg)
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "qemu-compose"
3
+ version = "0.1.0"
4
+ readme = "README.md"
5
+ description = "docker-compose style composer for qemu"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "pyyaml>=6.0.2",
9
+ ]
10
+
11
+ [project.scripts]
12
+ qemu-compose = "qemu_compose.main:cli"
13
+
14
+ [tool.uv]
15
+ environments = [
16
+ "sys_platform == 'darwin'",
17
+ "sys_platform == 'linux'",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["qemu_compose/"]
26
+ exclude = [
27
+ "*.rst",
28
+ ]
29
+
30
+ [tool.hatch.build.targets.sdist]
31
+ packages = ["qemu_compose/"]
32
+ exclude = [
33
+ "*.rst",
34
+ ]
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])