atex 0.7__py3-none-any.whl → 0.8__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.
atex/minitmt/executor.py DELETED
@@ -1,348 +0,0 @@
1
- import os
2
- import re
3
- import time
4
- import select
5
- import threading
6
- import contextlib
7
- import subprocess
8
-
9
- from .. import util
10
- from . import testcontrol, scripts, fmf
11
-
12
-
13
- class Duration:
14
- """
15
- A helper for parsing, keeping and manipulating test run time based on
16
- FMF-defined 'duration' attribute.
17
- """
18
-
19
- def __init__(self, fmf_duration):
20
- """
21
- 'fmf_duration' is the string specified as 'duration' in FMF metadata.
22
- """
23
- duration = self._fmf_to_seconds(fmf_duration)
24
- self.end = time.monotonic() + duration
25
- # keep track of only the first 'save' and the last 'restore',
26
- # ignore any nested ones (as tracked by '_count')
27
- self.saved = None
28
- self.saved_count = 0
29
-
30
- @staticmethod
31
- def _fmf_to_seconds(string):
32
- match = re.fullmatch(r"([0-9]+)([a-z]*)", string)
33
- if not match:
34
- raise RuntimeError(f"'duration' has invalid format: {string}")
35
- length, unit = match.groups()
36
- if unit == "m":
37
- return int(length)*60
38
- elif unit == "h":
39
- return int(length)*60*60
40
- elif unit == "d":
41
- return int(length)*60*60*24
42
- else:
43
- return int(length)
44
-
45
- def set(self, to):
46
- self.end = time.monotonic() + self._fmf_to_seconds(to)
47
-
48
- def increment(self, by):
49
- self.end += self._fmf_to_seconds(by)
50
-
51
- def decrement(self, by):
52
- self.end -= self._fmf_to_seconds(by)
53
-
54
- def save(self):
55
- if self.saved_count == 0:
56
- self.saved = self.end - time.monotonic()
57
- self.saved_count += 1
58
-
59
- def restore(self):
60
- if self.saved_count > 1:
61
- self.saved_count -= 1
62
- elif self.saved_count == 1:
63
- self.end = time.monotonic() + self.saved
64
- self.saved_count = 0
65
- self.saved = None
66
-
67
- def out_of_time(self):
68
- return time.monotonic() > self.end
69
-
70
-
71
- # SETUP global:
72
- # - create CSVAggregator instance on some destination dir
73
-
74
- # SETUP of new Provisioner instance:
75
- # - with SSHConn
76
- # - rsync test repo to some tmpdir on the remote
77
- # - install packages from plan
78
- # - create TMT_PLAN_ENVIRONMENT_FILE and export it to plan setup scripts
79
- # - run plan setup scripts
80
-
81
- # SETUP + CLEANUP for one test
82
- # - create Executor instance
83
- # - pass TMT_PLAN_ENVIRONMENT_FILE to it
84
- # - probably as general "environment variables" input,
85
- # could be reused in the future for ie. TMT_PLAN_NAME or so
86
- # - pass env from CLI -e switches
87
- # - pass disconnected SSHConn to it
88
- # - pass one FMFTest namedtuple to it (test to run)
89
- # - pass test repo location on the remote host
90
- # - pass CSVAggregator instance to it, so it can write results/logs
91
- # - with SSHConn
92
- # - create wrapper script on the remote
93
- # - run wrapper script, redirecting stderr to tmpfile via CSVAggregator
94
- # - poll() / select() over test stdout, parse TEST_CONTROL protocol
95
- # - on 0.1sec poll timeout
96
- # - check if SSHConn master is alive, re-connect if not
97
- # - check test duration against fmf-defined (control-adjusted) duration
98
- # - ...
99
- # - make Executor return some complex status
100
- # - whether testing was destructive (just leave SSHConn disconnected?)
101
-
102
-
103
- class TestAbortedError(Exception):
104
- """
105
- Raised when an infrastructure-related issue happened while running a test.
106
- """
107
- pass
108
-
109
-
110
- # TODO: automatic reporting of all partial results that were not finished
111
- class Executor:
112
- """
113
- Logic for running tests on a remote system and processing results
114
- and uploaded files by those tests.
115
- """
116
-
117
- def __init__(self, remote, aggregator, remote_dir=None, env=None):
118
- """
119
- 'remote' is a reserved class Remote instance, with an active connection.
120
-
121
- 'aggregator' is an instance of a ResultAggregator all the results
122
- and uploaded files will be written to.
123
-
124
- 'remote_dir' is a path on the remote for storing tests and other
125
- metadata. If unspecified, a tmpdir is created and used instead.
126
-
127
- 'env' is a dict of extra environment variables to pass to the
128
- plan prepare scripts and tests.
129
- """
130
- self.lock = threading.RLock()
131
- self.remote = remote
132
- self.aggregator = aggregator
133
- self.cancelled = False
134
- self.remote_dir = remote_dir
135
- self.plan_env = env.copy() if env else {}
136
-
137
- def _get_remote_dir(self):
138
- # TODO: do not mktemp here, do it in the parent, have remote_dir be mandatory,
139
- # renamed to 'tests_dir' (just the test repo), cleaned up by external logic
140
- # - handle custom metadata remote dir in run_test() and clean it up there
141
- # (for TMT_PLAN_ENVIRONMENT_FILE, wrapper, etc.)
142
- if not self.remote_dir:
143
- self.remote_dir = self.remote.cmd(
144
- ("mktemp", "-d", "-p", "/var/tmp"),
145
- func=util.subprocess_output,
146
- )
147
- return self.remote_dir
148
-
149
- # TODO: do not do this in Executor
150
- def upload_tests(self, tests_dir):
151
- """
152
- Upload a directory of all tests from a local 'tests_dir' path to
153
- a temporary directory on the remote host.
154
- """
155
- remote_dir = self._get_remote_dir()
156
- self.remote.rsync(
157
- "-rv", "--delete", "--exclude=.git/",
158
- f"{tests_dir}/",
159
- f"remote:{remote_dir}/tests",
160
- )
161
-
162
- def setup_plan(self, fmf_tests):
163
- """
164
- Install packages and run scripts, presumably extracted from a TMT plan
165
- provided as 'fmf_tests', an initialized FMFTests instance.
166
-
167
- Also prepare additional environment for tests, ie. create and export
168
- a path to TMT_PLAN_ENVIRONMENT_FILE.
169
- """
170
- # install packages from the plan
171
- if fmf_tests.prepare_pkgs:
172
- self.remote.cmd(
173
- (
174
- "dnf", "-y", "--setopt=install_weak_deps=False",
175
- "install", *fmf_tests.prepare_pkgs,
176
- ),
177
- check=True,
178
- )
179
-
180
- # create TMT_PLAN_ENVIRONMENT_FILE
181
- self.plan_env.update(fmf_tests.plan_env)
182
- env_file = f"{self._get_remote_dir()}/TMT_PLAN_ENVIRONMENT_FILE"
183
- self.remote.cmd(("truncate", "-s", "0", env_file), check=True)
184
- self.plan_env["TMT_PLAN_ENVIRONMENT_FILE"] = env_file
185
-
186
- # run prepare scripts
187
- env_args = (f"{k}={v}" for k, v in self.plan_env.items())
188
- for script in fmf_tests.prepare_scripts:
189
- self.remote.cmd(
190
- ("env", *env_args, "bash"),
191
- input=script,
192
- text=True,
193
- check=True,
194
- )
195
-
196
- def run_test(self, fmf_test, env=None):
197
- """
198
- Run one test on the remote system.
199
-
200
- 'fmf_test' is a FMFTest namedtuple with the test to run.
201
-
202
- 'env' is a dict of extra environment variables to pass to the test.
203
- """
204
- env_vars = self.plan_env.copy()
205
- for item in fmf.listlike(fmf_test.data, "environment"):
206
- env_vars.update(item)
207
- env_vars["ATEX_TEST_NAME"] = fmf_test.name
208
- env_vars["TMT_TEST_NAME"] = fmf_test.name
209
- if env:
210
- env_vars.update(env)
211
- env_args = (f"{k}={v}" for k, v in env_vars.items())
212
-
213
- # run a setup script, preparing wrapper + test scripts
214
- remote_dir = self._get_remote_dir()
215
- setup_script = scripts.test_setup(
216
- test=fmf_test,
217
- tests_dir=f"{remote_dir}/tests",
218
- wrapper_exec=f"{remote_dir}/wrapper.sh",
219
- test_exec=f"{remote_dir}/test.sh",
220
- debug=True,
221
- )
222
- self.remote.cmd(("bash",), input=setup_script, text=True, check=True)
223
-
224
- with contextlib.ExitStack() as stack:
225
- testout_fd = stack.enter_context(self.aggregator.open_tmpfile())
226
-
227
- duration = Duration(fmf_test.data.get("duration", "5m"))
228
-
229
- test_proc = None
230
- control_fd = None
231
- stack.callback(lambda: os.close(control_fd) if control_fd else None)
232
-
233
- def abort(msg):
234
- if test_proc:
235
- test_proc.kill()
236
- test_proc.wait()
237
- self.remote.release()
238
- raise TestAbortedError(msg) from None
239
-
240
- try:
241
- # TODO: probably enum
242
- state = "starting_test"
243
- while not duration.out_of_time():
244
- with self.lock:
245
- if self.cancelled:
246
- abort("cancel requested")
247
- return
248
-
249
- if state == "starting_test":
250
- control_fd, pipe_w = os.pipe()
251
- os.set_blocking(control_fd, False)
252
- control = testcontrol.TestControl(
253
- control_fd=control_fd,
254
- aggregator=self.aggregator,
255
- duration=duration,
256
- testout_fd=testout_fd,
257
- )
258
- # run the test in the background, letting it log output directly to
259
- # an opened file (we don't handle it, cmd client sends it to kernel)
260
- test_proc = self.remote.cmd(
261
- ("env", *env_args, f"{remote_dir}/wrapper.sh"),
262
- stdout=pipe_w,
263
- stderr=testout_fd,
264
- func=util.subprocess_Popen,
265
- )
266
- os.close(pipe_w)
267
- state = "reading_control"
268
-
269
- elif state == "reading_control":
270
- rlist, _, xlist = select.select((control_fd,), (), (control_fd,), 0.1)
271
- if xlist:
272
- abort(f"got exceptional condition on control_fd {control_fd}")
273
- elif rlist:
274
- control.process()
275
- if control.eof:
276
- os.close(control_fd)
277
- control_fd = None
278
- state = "waiting_for_exit"
279
-
280
- elif state == "waiting_for_exit":
281
- # control stream is EOF and it has nothing for us to read,
282
- # we're now just waiting for proc to cleanly terminate
283
- try:
284
- code = test_proc.wait(0.1)
285
- if code == 0:
286
- # wrapper exited cleanly, testing is done
287
- break
288
- else:
289
- # unexpected error happened (crash, disconnect, etc.)
290
- self.remote.disconnect()
291
- # if reconnect was requested, do so, otherwise abort
292
- if control.reconnect:
293
- state = "reconnecting"
294
- if control.reconnect != "always":
295
- control.reconnect = None
296
- else:
297
- abort(f"test wrapper unexpectedly exited with {code}")
298
- test_proc = None
299
- except subprocess.TimeoutExpired:
300
- pass
301
-
302
- elif state == "reconnecting":
303
- try:
304
- self.remote.connect(block=False)
305
- state = "reading_control"
306
- except BlockingIOError:
307
- pass
308
-
309
- else:
310
- raise AssertionError("reached unexpected state")
311
-
312
- else:
313
- abort("test duration timeout reached")
314
-
315
- # testing successful, do post-testing tasks
316
-
317
- # test wrapper hasn't provided exitcode
318
- if control.exit_code is None:
319
- abort("exitcode not reported, wrapper bug?")
320
-
321
- # partial results that were never reported
322
- if control.partial_results:
323
- control.result_seen = True # partial result is also a result
324
- for result in control.partial_results.values():
325
- self.aggregator.report(result)
326
-
327
- # test hasn't reported a single result, add an automatic one
328
- # as specified in RESULTS.md
329
- # {"status": "pass", "name": "/some/test", "testout": "output.txt"}
330
- if not control.result_seen:
331
- self.aggregator.link_tmpfile_to(fmf_test.name, "output.txt", testout_fd)
332
- self.aggregator.report({
333
- "status": "pass" if control.exit_code == 0 else "fail",
334
- "name": fmf_test.name,
335
- "testout": "output.txt",
336
- })
337
-
338
- except Exception:
339
- # if the test hasn't reported a single result, but still
340
- # managed to break something, provide at least the default log
341
- # for manual investigation - otherwise test output disappears
342
- if not control.result_seen:
343
- self.aggregator.link_tmpfile_to(fmf_test.name, "output.txt", testout_fd)
344
- raise
345
-
346
- def cancel(self):
347
- with self.lock:
348
- self.cancelled = True
@@ -1,74 +0,0 @@
1
- The idea is to use systemd-nspawn containers on the host, binding
2
- /dev/kvm to each, thus avoiding the need for nested virt as our first layer
3
- of Contest tests will run in the containers (installing libvirtd, etc.)
4
- and the second layer (VMs created by tests) will use virtual machines,
5
- via a non-nested HVM.
6
-
7
- systemd-nspawn containers can have CPU core limits, memory limits, etc.
8
- done via cgroups, so we can provide some level of isolation/safety.
9
-
10
-
11
- systemd-nspawn can create its own veth via --network-veth=... and put it into
12
- a bridge automatically via --network-bridge=...
13
-
14
- We can then use NetworkManager + firewalld to pre-create a bridge with built-in
15
- DHCP and NAT to the outside, via something like
16
-
17
- nmcli connection add type bridge ifname br0 con-name br0 ipv4.method shared ipv6.method ignore
18
-
19
- According to https://fedoramagazine.org/internet-connection-sharing-networkmanager/
20
- the ipv4.method=shared :
21
-
22
- enables IP forwarding for the interface;
23
- adds firewall rules and enables masquerading;
24
- starts dnsmasq as a DHCP and DNS server.
25
-
26
- Specifically it should add MASQUERADE on packets *outgoing* from the bridge subnet,
27
- so shouldn't need any modification of the upstream eth0 device or any fw rules tied to it.
28
-
29
- There also seems to be ipv4.addresses 192.168.42.1/24 to modify the subnet?
30
-
31
- If that doesn't work, firewalld has an External zone that has <masquerade/>
32
- by default, so
33
-
34
- nmcli connection modify br0 connection.zone external
35
-
36
- should work.
37
-
38
-
39
- --------
40
-
41
- TODO: We need some way to get DHCP leases for started containers (so we can connect
42
- to the containerized sshd).
43
-
44
- If there is no command for it via nmcli, it should be possible to just
45
- extract it from wherever NetworkManager pointed dnsmasq to store its leases file.
46
-
47
- We can then probably correlate --network-veth=... device from systemd-nspawn
48
- (named after --machine=... name, prefixed with ve-* or vb-* if --network-bridge=* is used)
49
- to the leased IP address.
50
-
51
- ls -l /var/lib/NetworkManager/dnsmasq-*.leases
52
-
53
- Or perhaps parse it out of 'ip neigh' to make sure the guest is *really* up.
54
- - 'ip neigh' gives us MAC-to-IP, but device is always br0
55
- - 'ip link show dev vb-contname' should give us the MAC for 'ip neigh'
56
- - if container veth endpoint uses different mac, we can query bridge forward DB
57
- via 'bridge fdb' to get all MACs that appeared on the veth
58
-
59
- --------
60
-
61
- Containers can be installed via ie.
62
-
63
- dnf --releasever=41 --installroot=/var/lib/machines/f41 --use-host-config \
64
- --setopt=install_weak_deps=False \
65
- install \
66
- passwd dnf fedora-release vim-minimal util-linux systemd NetworkManager
67
-
68
- where --use-host-config re-uses host repositories.
69
-
70
- Maybe consider 'machinectl'-managed containers (start/terminate/kill/reboot/etc.)
71
- which are just repackaged systemd-nspawn@ services.
72
- - Especially since there is no concept of "throw away disk snapshot with container exit",
73
- we always need some copy/clone of the --installroot for each instance of the container,
74
- so using ie. 'machinectl clone ...' would provide a nice interface for it.
@@ -1 +0,0 @@
1
- a = 123
atex-0.7.dist-info/RECORD DELETED
@@ -1,32 +0,0 @@
1
- atex/__init__.py,sha256=LdX67gprtHYeAkjLhFPKzpc7ECv2rHxUbHKDGbGXO1c,517
2
- atex/cli/__init__.py,sha256=erHv68SsybRbdgJ60013y9jVqY1ec-cb9T9ThPCJ_HY,2408
3
- atex/cli/minitmt.py,sha256=h1jHRGR0nqc7ANVTjmB5aeLron2A04vpzGGMQJLBliY,5793
4
- atex/cli/testingfarm.py,sha256=4oKWbkJwj0gs4_NnFP7w8fo2VvD5NpaPNG4HCeKm94I,6706
5
- atex/connection/__init__.py,sha256=O7rJDwf438lX2YgM8uHCC9gkmvaLYt3jRvtDq_FSzME,4107
6
- atex/connection/ssh.py,sha256=qVKeAY16pLGGS-d4TIF5A2DWWlNGR5qNlAmHo-jAIEc,13985
7
- atex/minitmt/__init__.py,sha256=wCda0QXwxlxkH4PpY0lGlNWT96_pUj7taNNP0Mo6TQk,522
8
- atex/minitmt/executor.py,sha256=zmFHo3M5hHDOzfZrXBPFSeqLmLK__q2KkT5A93ELEzY,13415
9
- atex/minitmt/fmf.py,sha256=HAjQkfrDLQLW2ZBvgQmleVpL_tfNSP9bUuqRt9BdjS4,7559
10
- atex/minitmt/scripts.py,sha256=WsLsQOmNxinpGUhOqqwN788zjcd5o79VC7I7UeGRwb8,5146
11
- atex/minitmt/testcontrol.py,sha256=l670ou1oPr5q3RA4QNQ0IgSh3uYvuvzcUW67NLNVgCA,12209
12
- atex/orchestrator/__init__.py,sha256=pMBBtev20KWAmQdth3RfuTwltXDm57CakDBpAsqyNYM,1592
13
- atex/orchestrator/aggregator.py,sha256=pTL6Ru70sin5WOc-dl3-bNSEvuOfWq7YLtduwiLX9q8,5463
14
- atex/provision/__init__.py,sha256=hJ7wbn6_LaCQVrIFilEcDA4z4LxglHYNEsxmizwr_OE,5573
15
- atex/provision/libvirt/VM_PROVISION,sha256=7pkZ-ozgTyK4qNGC-E-HUznr4IhbosWSASbB72Gknl8,2664
16
- atex/provision/libvirt/__init__.py,sha256=mAkGtciZsXdR9MVVrjm3OWNXZqTs_33-J1qAszFA0k4,768
17
- atex/provision/libvirt/setup-libvirt.sh,sha256=CXrEFdrj8CSHXQZCd2RWuRvTmw7QYFTVhZeLuhhXooI,1855
18
- atex/provision/nspawn/README,sha256=mIjOA6R5sM-1wGDm-7zBcEqax1NMSRzijwyeqVjGoqE,3127
19
- atex/provision/podman/README,sha256=kgP3vcTfWW9gcQzmXnyucjgWbqjNqm_ZM--pnqNTXRg,1345
20
- atex/provision/podman/host_container.sh,sha256=buCNz0BlsHY5I64sMSTGQHkvzEK0aeIhpGJXWCQVMXk,2283
21
- atex/provision/testingfarm/__init__.py,sha256=l4T4VOUQWKYEQXPe0OtfJmuW-aGSiSQ2mdOf6fSjpjU,605
22
- atex/provision/testingfarm/api.py,sha256=S-9m1D1CaG-1gEgh7oT1RkXPv2sfUw7Od7bW-Wv0uJQ,19262
23
- atex/provision/testingfarm/foo.py,sha256=WfGpn_saQb5x9Svx-gfViyXPVRF_u-gUjagUwLBClM0,8
24
- atex/util/__init__.py,sha256=PWU0STjLcQfe2pPaWKygRXQAudy8CNFdbnTGQ488mHs,1550
25
- atex/util/dedent.py,sha256=SEuJMtLzqz3dQ7g7qyZzEJ9VYynVlk52tQCJY-FveXo,603
26
- atex/util/log.py,sha256=oqJGXgHZkTgXVbD2-gzA2pqV3L8w0r_czYWIPfVz398,1776
27
- atex/util/subprocess.py,sha256=IQT9QHe2kMaaO_XPSry-DwObYstGsq6_QdwdbhYDjko,1826
28
- atex-0.7.dist-info/METADATA,sha256=VvPhdyVKdg_-kmR0y1WkClrYxw24yjoee3jGzkj6--g,5076
29
- atex-0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- atex-0.7.dist-info/entry_points.txt,sha256=pLqJdcfeyQTgup2h6dWb6SvkHhtOl-W5Eg9zV8moK0o,39
31
- atex-0.7.dist-info/licenses/COPYING.txt,sha256=oEuj51jdmbXcCUy7pZ-KE0BNcJTR1okudRp5zQ0yWnU,670
32
- atex-0.7.dist-info/RECORD,,
File without changes