atex 0.9__py3-none-any.whl → 0.11__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.
Files changed (38) hide show
  1. atex/aggregator/__init__.py +62 -0
  2. atex/aggregator/json.py +279 -0
  3. atex/cli/__init__.py +14 -1
  4. atex/cli/fmf.py +7 -7
  5. atex/cli/libvirt.py +3 -2
  6. atex/cli/testingfarm.py +74 -3
  7. atex/connection/podman.py +2 -4
  8. atex/connection/ssh.py +7 -14
  9. atex/executor/executor.py +21 -20
  10. atex/executor/scripts.py +5 -3
  11. atex/executor/testcontrol.py +1 -1
  12. atex/orchestrator/__init__.py +76 -3
  13. atex/orchestrator/{orchestrator.py → adhoc.py} +246 -108
  14. atex/orchestrator/contest.py +94 -0
  15. atex/{provision → provisioner}/__init__.py +48 -52
  16. atex/{provision → provisioner}/libvirt/libvirt.py +34 -15
  17. atex/{provision → provisioner}/libvirt/locking.py +3 -1
  18. atex/provisioner/podman/__init__.py +2 -0
  19. atex/provisioner/podman/podman.py +169 -0
  20. atex/{provision → provisioner}/testingfarm/api.py +56 -48
  21. atex/{provision → provisioner}/testingfarm/testingfarm.py +43 -45
  22. atex/util/log.py +62 -67
  23. atex/util/subprocess.py +46 -12
  24. atex/util/threads.py +7 -0
  25. atex-0.11.dist-info/METADATA +86 -0
  26. atex-0.11.dist-info/RECORD +45 -0
  27. {atex-0.9.dist-info → atex-0.11.dist-info}/WHEEL +1 -1
  28. atex/orchestrator/aggregator.py +0 -111
  29. atex/provision/podman/__init__.py +0 -1
  30. atex/provision/podman/podman.py +0 -274
  31. atex-0.9.dist-info/METADATA +0 -178
  32. atex-0.9.dist-info/RECORD +0 -43
  33. /atex/{provision → provisioner}/libvirt/VM_PROVISION +0 -0
  34. /atex/{provision → provisioner}/libvirt/__init__.py +0 -0
  35. /atex/{provision → provisioner}/libvirt/setup-libvirt.sh +0 -0
  36. /atex/{provision → provisioner}/testingfarm/__init__.py +0 -0
  37. {atex-0.9.dist-info → atex-0.11.dist-info}/entry_points.txt +0 -0
  38. {atex-0.9.dist-info → atex-0.11.dist-info}/licenses/COPYING.txt +0 -0
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: atex
3
+ Version: 0.11
4
+ Summary: Ad-hoc Test EXecutor
5
+ Project-URL: Homepage, https://github.com/RHSecurityCompliance/atex
6
+ License-Expression: GPL-3.0-or-later
7
+ License-File: COPYING.txt
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Software Development :: Testing
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: fmf>=1.6
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: urllib3<3,>=2
15
+ Description-Content-Type: text/markdown
16
+
17
+ # ATEX = Ad-hoc Test EXecutor
18
+
19
+ A collections of Python APIs to provision operating systems, collect
20
+ and execute [FMF](https://github.com/teemtee/fmf/)-style tests, gather
21
+ and organize their results and generate reports from those results.
22
+
23
+ The name comes from a (fairly unique to FMF/TMT ecosystem) approach that
24
+ allows provisioning a pool of systems and scheduling tests on them as one would
25
+ on an ad-hoc pool of thread/process workers - once a worker becomes free,
26
+ it receives a test to run.
27
+ This is in contrast to splitting a large list of N tests onto M workers
28
+ like N/M, which yields significant time penalties due to tests having
29
+ very varies runtimes.
30
+
31
+ Above all, this project is meant to be a toolbox, not a silver-plate solution.
32
+ Use its Python APIs to build a CLI tool for your specific use case.
33
+ The CLI tool provided here is just for demonstration / testing, not for serious
34
+ use - we want to avoid huge modular CLIs for Every Possible Scenario. That's
35
+ the job of the Python API. Any CLI should be simple by nature.
36
+
37
+ ---
38
+
39
+ ## License
40
+
41
+ Unless specified otherwise, any content within this repository is distributed
42
+ under the GNU GPLv3 license, see the [COPYING.txt](COPYING.txt) file for more.
43
+
44
+ ## Environment variables
45
+
46
+ - `ATEX_DEBUG_TEST`
47
+ - Set to `1` to print out detailed runner-related trace within the test output
48
+ stream (as if it was printed out by the test).
49
+
50
+ ## Testing this project
51
+
52
+ There are some limited sanity tests provided via `pytest`, although:
53
+
54
+ - Some require additional variables (ie. Testing Farm) and will ERROR
55
+ without them.
56
+ - Some take a long time (ie. Testing Farm) due to system provisioning
57
+ taking a long time, so install `pytest-xdist` and run with a large `-n`.
58
+
59
+ Currently, the recommended approach is to split the execution:
60
+
61
+ ```
62
+ # synchronously, because podman CLI has concurrency issues
63
+ pytest tests/provision/test_podman.py
64
+
65
+ # in parallel, because provisioning takes a long time
66
+ export TESTING_FARM_API_TOKEN=...
67
+ export TESTING_FARM_COMPOSE=...
68
+ pytest -n 20 tests/provision/test_podman.py
69
+
70
+ # fast enough for synchronous execution
71
+ pytest tests/fmf
72
+ ```
73
+
74
+ ## Unsorted notes
75
+
76
+ TODO: codestyle from contest
77
+
78
+ ```
79
+ - this is not tmt, the goal is to make a python toolbox *for* making runcontest
80
+ style tools easily, not to replace those tools with tmt-style CLI syntax
81
+
82
+ - the whole point is to make usecase-targeted easy-to-use tools that don't
83
+ intimidate users with 1 KB long command line, and runcontest is a nice example
84
+
85
+ - TL;DR - use a modular pythonic approach, not a gluetool-style long CLI
86
+ ```
@@ -0,0 +1,45 @@
1
+ atex/__init__.py,sha256=LdX67gprtHYeAkjLhFPKzpc7ECv2rHxUbHKDGbGXO1c,517
2
+ atex/fmf.py,sha256=gkJXIaRO7_KvwJR-V6Tc1NVn4a9Hq7hoBLQLhxYIdbg,8834
3
+ atex/aggregator/__init__.py,sha256=8mN-glHdzR4icKAUGO4JPodsTrLMdJoeuZsO2CTbhyU,1773
4
+ atex/aggregator/json.py,sha256=tpoUZoZM8EMYhZKwVr4LRtgEIDjRxC11BIKVXZKYPOs,10441
5
+ atex/cli/__init__.py,sha256=Ew2z-gC0jvOmU_DqYgXVQla3p1rTnrz64I63q52aHv4,2899
6
+ atex/cli/fmf.py,sha256=pvj_OIp6XT_nVUwziL7-v_HNbyAtuUmb7k_Ey_KkFJc,3616
7
+ atex/cli/libvirt.py,sha256=6tt5ANb8XBBRXOQsYPTWILThKqf-gvt5AZh5Dctg2PA,3782
8
+ atex/cli/testingfarm.py,sha256=ovgoogmIM2TglS7iQD3liMiEYYtcykS_HRRKbltpW2I,10131
9
+ atex/connection/__init__.py,sha256=dj8ZBcEspom7Z_UjecfLGBRNvLZ3dyGR9q19i_B4xpY,3880
10
+ atex/connection/podman.py,sha256=1T56gh1TgbcQWpTIJHL4NaxZOI6aMg7Xp7sn6PQQyBk,1911
11
+ atex/connection/ssh.py,sha256=9A57b9YR_HI-kIu06Asic1y__JPVXEheDZxjbG2Qcsc,13460
12
+ atex/executor/__init__.py,sha256=XCfhi7QDELjey7N1uzhMjc46Kp1Jsd5bOCf52I27SCE,85
13
+ atex/executor/duration.py,sha256=x06sItKOZi6XA8KszQwZGpIb1Z_L-HWqIwZKo2SDo0s,1759
14
+ atex/executor/executor.py,sha256=toyLVQCDzfw381iEGrvOXoKPsd4SqxMZHwlDSTJGqKk,15792
15
+ atex/executor/reporter.py,sha256=MceFmHFt0bTEClBZbRI1WnFbfMhR0e1noOzcu7gjKuQ,3403
16
+ atex/executor/scripts.py,sha256=riJAQWsV-BFGkJwR2Dmf3R0ZRRZJs9w9iYnPpYaQNaE,5618
17
+ atex/executor/testcontrol.py,sha256=mVrLwQUnDRfUq-5diz-80UvCWWxn1TkcBgmAKhKNb5E,12696
18
+ atex/orchestrator/__init__.py,sha256=8Q1YknyibilXLjWRYkHm_Mr2HMm0SRw8Zv39KypeASM,2059
19
+ atex/orchestrator/adhoc.py,sha256=QpYoPeyQzYFDBM1zgFJKMXH1RtdJixbH5whVX0OP-14,21003
20
+ atex/orchestrator/contest.py,sha256=ADmRlsZPQx-MJ6fWHmBcJOIy3DSPnvwVheVL9Upwtg0,3703
21
+ atex/provisioner/__init__.py,sha256=6hZxQlvTQ0yWWqCRCPqWMoYuim5wDMCcDIYHF-nIfMs,4013
22
+ atex/provisioner/libvirt/VM_PROVISION,sha256=7pkZ-ozgTyK4qNGC-E-HUznr4IhbosWSASbB72Gknl8,2664
23
+ atex/provisioner/libvirt/__init__.py,sha256=pKG5IpZSC2IHs5wL2ecQx_fd9AzAXEbZmDzA7RyZsfM,119
24
+ atex/provisioner/libvirt/libvirt.py,sha256=ZKctK2B51olvWvLxz2pZ2s6LtX_7EJ43LvlyJHnI1Ho,18955
25
+ atex/provisioner/libvirt/locking.py,sha256=AXtDyidZNmUoMmrit26g9iTHDqInrzL_RSQEoc_EAXw,5669
26
+ atex/provisioner/libvirt/setup-libvirt.sh,sha256=oCMy9SCnbC_QuAzO2sFwvB5ui1kMQ6uviHsgdXyoFXc,2428
27
+ atex/provisioner/podman/__init__.py,sha256=dM0JzQXWX7edtWSc0KH0cMFXAjArFn2Vme4j_ZMsdYA,138
28
+ atex/provisioner/podman/podman.py,sha256=ztRypoakSf-jF04iER58tEMUZ4Y6AuzIpNpFXp44bB4,4997
29
+ atex/provisioner/testingfarm/__init__.py,sha256=kZncgLGdRCR4FMaRQr2GTwJ8vjlA-24ri8JO2ueZJuw,113
30
+ atex/provisioner/testingfarm/api.py,sha256=dlXe9brzHERawIx2UTv34u2tOSskdZtXD68-u1MnOHk,21726
31
+ atex/provisioner/testingfarm/testingfarm.py,sha256=yvQzWat92B4UnJNZzCLI8mpAKf_QvHUKyKbjlk5123Q,8573
32
+ atex/util/__init__.py,sha256=cWHFbtQ4mDlKe6lXyPDWRmWJOTcHDGfVuW_-GYa8hB0,1473
33
+ atex/util/dedent.py,sha256=SEuJMtLzqz3dQ7g7qyZzEJ9VYynVlk52tQCJY-FveXo,603
34
+ atex/util/libvirt.py,sha256=kDZmT6xLYEZkQNLZY98gJ2M48DDWXxHF8rQY9PnjB3U,660
35
+ atex/util/log.py,sha256=KVR7ep8n5wtghsvBFCtHiPsMAQBdAmK83E_Jec5t4cU,2230
36
+ atex/util/named_mapping.py,sha256=UBMe9TetjV-DGPhjYjJ42YtC40FVPKAAEROXl9MA5fo,4700
37
+ atex/util/path.py,sha256=x-kXqiWCVodfZWbEwtC5A8LFvutpDIPYv2m0boZSlXU,504
38
+ atex/util/ssh_keygen.py,sha256=9yuSl2yBV7pG3Qfsf9tossVC00nbIUrAeLdbwTykpjk,384
39
+ atex/util/subprocess.py,sha256=_oQN8CNgGoH9GAR6nZlpujYe2HjXFBcCuIkLPw-IxJ4,2971
40
+ atex/util/threads.py,sha256=c8hsEc-8SqJGodInorv_6JxpiHiSkGFGob4qbMmOD2M,3531
41
+ atex-0.11.dist-info/METADATA,sha256=3fRMLBrkoRIHwbY2GheyNkrx4mNVmL_95wlxGjZsORc,3050
42
+ atex-0.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
43
+ atex-0.11.dist-info/entry_points.txt,sha256=pLqJdcfeyQTgup2h6dWb6SvkHhtOl-W5Eg9zV8moK0o,39
44
+ atex-0.11.dist-info/licenses/COPYING.txt,sha256=oEuj51jdmbXcCUy7pZ-KE0BNcJTR1okudRp5zQ0yWnU,670
45
+ atex-0.11.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,111 +0,0 @@
1
- import gzip
2
- import json
3
- import shutil
4
- import threading
5
- from pathlib import Path
6
-
7
-
8
- class JSONAggregator:
9
- """
10
- Collects reported results as a GZIP-ed line-JSON and files (logs) from
11
- multiple test runs under a shared directory.
12
-
13
- Note that the aggregated JSON file *does not* use the test-based JSON format
14
- described by executor/RESULTS.md - both use JSON, but are very different.
15
-
16
- This aggergated format uses a top-level array (on each line) with a fixed
17
- field order:
18
-
19
- platform, status, test name, subtest name, files, note
20
-
21
- All these are strings except 'files', which is another (nested) array
22
- of strings.
23
-
24
- If a field is missing in the source result, it is translated to a null
25
- value.
26
- """
27
-
28
- def __init__(self, json_file, storage_dir):
29
- """
30
- 'json_file' is a string/Path to a .json.gz file with aggregated results.
31
-
32
- 'storage_dir' is a string/Path of the top-level parent for all
33
- per-platform / per-test files uploaded by tests.
34
- """
35
- self.lock = threading.RLock()
36
- self.storage_dir = Path(storage_dir)
37
- self.json_file = Path(json_file)
38
- self.json_gzip_fobj = None
39
-
40
- def open(self):
41
- if self.json_file.exists():
42
- raise FileExistsError(f"{self.json_file} already exists")
43
- self.json_gzip_fobj = gzip.open(self.json_file, "wt", newline="\n")
44
-
45
- if self.storage_dir.exists():
46
- raise FileExistsError(f"{self.storage_dir} already exists")
47
- self.storage_dir.mkdir()
48
-
49
- def close(self):
50
- if self.json_gzip_fobj:
51
- self.json_gzip_fobj.close()
52
- self.json_gzip_fobj = None
53
-
54
- def __enter__(self):
55
- try:
56
- self.open()
57
- return self
58
- except Exception:
59
- self.close()
60
- raise
61
-
62
- def __exit__(self, exc_type, exc_value, traceback):
63
- self.close()
64
-
65
- def ingest(self, platform, test_name, results_file, files_dir):
66
- """
67
- Process 'results_file' (string/Path) for reported results and append
68
- them to the overall aggregated line-JSON file, recursively copying over
69
- the dir structure under 'files_dir' (string/Path) under the respective
70
- platform and test name in the aggregated storage dir.
71
- """
72
- platform_dir = self.storage_dir / platform
73
- test_dir = platform_dir / test_name.lstrip("/")
74
- if test_dir.exists():
75
- raise FileExistsError(f"{test_dir} already exists for {test_name}")
76
-
77
- # parse the results separately, before writing any aggregated output,
78
- # to ensure that either all results from the test are ingested, or none
79
- # at all (ie. if one of the result lines contains JSON errors)
80
- output_lines = []
81
- with open(results_file) as results_fobj:
82
- for raw_line in results_fobj:
83
- result_line = json.loads(raw_line)
84
-
85
- file_names = []
86
- if "testout" in result_line:
87
- file_names.append(result_line["testout"])
88
- if "files" in result_line:
89
- file_names += (f["name"] for f in result_line["files"])
90
-
91
- output_line = (
92
- platform,
93
- result_line["status"],
94
- test_name,
95
- result_line.get("name"),
96
- file_names,
97
- result_line.get("note"),
98
- )
99
- encoded = json.dumps(output_line, indent=None)
100
- output_lines.append(encoded)
101
-
102
- output_str = "\n".join(output_lines) + "\n"
103
-
104
- with self.lock:
105
- self.json_gzip_fobj.write(output_str)
106
- self.json_gzip_fobj.flush()
107
-
108
- Path(results_file).unlink()
109
-
110
- platform_dir.mkdir(exist_ok=True)
111
- shutil.move(files_dir, test_dir)
@@ -1 +0,0 @@
1
- from .podman import PodmanProvisioner, PodmanRemote # noqa: F401
@@ -1,274 +0,0 @@
1
- import os
2
- import time
3
- import enum
4
- import threading
5
- import subprocess
6
-
7
- from ... import connection, util
8
- from .. import Provisioner, Remote
9
-
10
-
11
- class PodmanRemote(Remote, connection.podman.PodmanConn):
12
- """
13
- Built on the official Remote API, pulling in the Connection API
14
- as implemented by ManagedSSHConn.
15
- """
16
-
17
- def __init__(self, image, container, *, release_hook):
18
- """
19
- 'image' is an image tag (used for repr()).
20
-
21
- 'container' is a podman container id / name.
22
-
23
- 'release_hook' is a callable called on .release() in addition
24
- to disconnecting the connection.
25
- """
26
- super().__init__(container=container)
27
- self.lock = threading.RLock()
28
- self.image = image
29
- self.container = container
30
- self.release_called = False
31
- self.release_hook = release_hook
32
-
33
- def release(self):
34
- with self.lock:
35
- if self.release_called:
36
- return
37
- else:
38
- self.release_called = True
39
- self.release_hook(self)
40
- self.disconnect()
41
- util.subprocess_run(
42
- ("podman", "container", "rm", "-f", "-t", "0", self.container),
43
- check=False, # ignore if it fails
44
- stdout=subprocess.DEVNULL,
45
- )
46
-
47
- # not /technically/ a valid repr(), but meh
48
- def __repr__(self):
49
- class_name = self.__class__.__name__
50
-
51
- if "/" in self.image:
52
- image = self.image.rsplit("/",1)[1]
53
- elif len(self.image) > 20:
54
- image = f"{self.image[:17]}..."
55
- else:
56
- image = self.image
57
-
58
- name = f"{self.container[:17]}..." if len(self.container) > 20 else self.container
59
-
60
- return f"{class_name}({image}, {name})"
61
-
62
-
63
- class PodmanProvisioner(Provisioner):
64
- class State(enum.Enum):
65
- WAITING_FOR_PULL = enum.auto()
66
- CREATING_CONTAINER = enum.auto()
67
- WAITING_FOR_CREATION = enum.auto()
68
- SETTING_UP_CONTAINER = enum.auto()
69
- WAITING_FOR_SETUP = enum.auto()
70
-
71
- # NOTE: this uses a single Popen process to run podman commands,
72
- # to avoid double downloads/pulls, but also to avoid SQLite errors
73
- # when creating multiple containers in parallel
74
-
75
- def __init__(self, image, run_options=None, *, pull=True, max_systems=1):
76
- """
77
- 'image' is a string of image tag/id to create containers from.
78
- It can be a local identifier or an URL.
79
-
80
- 'run_options' is an iterable with additional CLI options passed
81
- to 'podman container run'.
82
-
83
- 'pull' specifies whether to attempt 'podman image pull' on the specified
84
- image tag/id before any container creation.
85
-
86
- 'max_systems' is a maximum number of containers running at any one time.
87
- """
88
- self.lock = threading.RLock()
89
- self.image = image
90
- self.run_options = run_options or ()
91
- self.pull = pull
92
- self.max_systems = max_systems
93
-
94
- self.image_id = None
95
- self.container_id = None
96
- self.worker = None
97
- self.worker_output = bytearray()
98
- self.state = None
99
- # created PodmanRemote instances, ready to be handed over to the user,
100
- # or already in use by the user
101
- self.remotes = []
102
-
103
- @staticmethod
104
- def _spawn_proc(cmd):
105
- proc = util.subprocess_Popen(
106
- cmd,
107
- stdin=subprocess.DEVNULL,
108
- stdout=subprocess.PIPE,
109
- stderr=subprocess.STDOUT,
110
- )
111
- os.set_blocking(proc.stdout.fileno(), False)
112
- return proc
113
-
114
- # @staticmethod
115
- # def _poll_proc(proc):
116
- # # read from the process to un-block any kernel buffers
117
- # try:
118
- # out = proc.stdout.read() # non-blocking
119
- # except BlockingIOError:
120
- # out = ""
121
- # return (proc.poll(), out)
122
-
123
- def _make_remote(self, container):
124
- def release_hook(remote):
125
- # remove from the list of remotes inside this Provisioner
126
- with self.lock:
127
- try:
128
- self.remotes.remove(remote)
129
- except ValueError:
130
- pass
131
-
132
- remote = PodmanRemote(
133
- self.image,
134
- container,
135
- release_hook=release_hook,
136
- )
137
- self.remotes.append(remote)
138
- return remote
139
-
140
- def start(self):
141
- if not self.image:
142
- raise ValueError("image cannot be empty")
143
-
144
- if not self.pull:
145
- self.image_id = self.image
146
- self.state = self.State.CREATING_CONTAINER
147
- else:
148
- self.worker = self._spawn_proc(
149
- ("podman", "image", "pull", "--quiet", self.image),
150
- )
151
- self.state = self.State.WAITING_FOR_PULL
152
-
153
- def stop(self):
154
- with self.lock:
155
- while self.remotes:
156
- self.remotes.pop().release()
157
- worker = self.worker
158
- self.worker = None
159
-
160
- if worker:
161
- worker.kill()
162
- # don"t zombie forever, return EPIPE on any attempts to write to us
163
- worker.stdout.close()
164
- worker.wait()
165
-
166
- def stop_defer(self):
167
- # avoid SQLite errors by removing containers sequentially
168
- return self.stop
169
-
170
- @staticmethod
171
- def _nonblock_read(fobj):
172
- """Return b'' if there was nothing to read, instead of None."""
173
- data = fobj.read()
174
- return b"" if data is None else data
175
-
176
- def _get_remote_nonblock(self):
177
- if self.state is None:
178
- raise RuntimeError("the provisioner is in an invalid state")
179
-
180
- # NOTE: these are not 'elif' statements explicitly to allow a next block
181
- # to follow a previous one (if the condition is met)
182
- if self.state is self.State.WAITING_FOR_PULL:
183
- self.worker_output += self._nonblock_read(self.worker.stdout)
184
- rc = self.worker.poll()
185
- if rc is None:
186
- return None # still running
187
- elif rc != 0:
188
- out = self.worker_output.decode().rstrip("\n")
189
- self.worker_output.clear()
190
- self.worker = None
191
- self.state = None
192
- raise RuntimeError(f"podman image pull failed with {rc}:\n{out}")
193
- else:
194
- self.image_id = self.worker_output.decode().rstrip("\n")
195
- self.worker_output.clear()
196
- self.worker = None
197
- self.state = self.State.CREATING_CONTAINER
198
-
199
- if self.state is self.State.CREATING_CONTAINER:
200
- if len(self.remotes) < self.max_systems:
201
- self.worker = self._spawn_proc(
202
- (
203
- "podman", "container", "run", "--quiet", "--detach", "--pull", "never",
204
- *self.run_options, self.image_id, "sleep", "inf",
205
- ),
206
- )
207
- self.state = self.State.WAITING_FOR_CREATION
208
- else:
209
- # too many remotes requested
210
- return None
211
-
212
- if self.state is self.State.WAITING_FOR_CREATION:
213
- self.worker_output += self._nonblock_read(self.worker.stdout)
214
- rc = self.worker.poll()
215
- if rc is None:
216
- return None # still running
217
- elif rc != 0:
218
- out = self.worker_output.decode().rstrip("\n")
219
- self.worker_output.clear()
220
- self.worker = None
221
- self.state = None
222
- raise RuntimeError(f"podman run failed with {rc}:\n{out}")
223
- else:
224
- self.container_id = self.worker_output.decode().rstrip("\n")
225
- self.worker_output.clear()
226
- self.worker = None
227
- self.state = self.State.SETTING_UP_CONTAINER
228
-
229
- if self.state is self.State.SETTING_UP_CONTAINER:
230
- cmd = ("dnf", "install", "-y", "-q", "--setopt=install_weak_deps=False", "rsync")
231
- self.worker = self._spawn_proc(
232
- ("podman", "container", "exec", self.container_id, *cmd),
233
- )
234
- self.state = self.State.WAITING_FOR_SETUP
235
-
236
- if self.state is self.State.WAITING_FOR_SETUP:
237
- self.worker_output += self._nonblock_read(self.worker.stdout)
238
- rc = self.worker.poll()
239
- if rc is None:
240
- return None # still running
241
- elif rc != 0:
242
- out = self.worker_output.decode().rstrip("\n")
243
- self.worker_output.clear()
244
- self.worker = None
245
- self.state = None
246
- raise RuntimeError(f"setting up failed with {rc}:\n{out}")
247
- else:
248
- # everything ready, give the Remote to the caller and reset
249
- remote = self._make_remote(self.container_id)
250
- self.worker_output.clear()
251
- self.worker = None
252
- self.state = self.State.CREATING_CONTAINER
253
- return remote
254
-
255
- raise AssertionError(f"reached end (invalid state {self.state}?)")
256
-
257
- def get_remote(self, block=True):
258
- if not block:
259
- with self.lock:
260
- return self._get_remote_nonblock()
261
- else:
262
- while True:
263
- with self.lock:
264
- if remote := self._get_remote_nonblock():
265
- return remote
266
- time.sleep(0.1)
267
-
268
- # not /technically/ a valid repr(), but meh
269
- def __repr__(self):
270
- class_name = self.__class__.__name__
271
- return (
272
- f"{class_name}({self.image}, {len(self.remotes)}/{self.max_systems} remotes, "
273
- f"{hex(id(self))})"
274
- )