atex 0.11__py3-none-any.whl → 0.13__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/cli/__init__.py +1 -1
- atex/cli/testingfarm.py +82 -52
- atex/connection/ssh.py +1 -0
- atex/executor/__init__.py +23 -2
- atex/executor/executor.py +23 -18
- atex/executor/reporter.py +3 -4
- atex/executor/scripts.py +14 -14
- atex/executor/testcontrol.py +32 -27
- atex/orchestrator/adhoc.py +38 -63
- atex/orchestrator/contest.py +26 -4
- atex/provisioner/testingfarm/api.py +62 -8
- atex/util/log.py +1 -1
- {atex-0.11.dist-info → atex-0.13.dist-info}/METADATA +1 -1
- {atex-0.11.dist-info → atex-0.13.dist-info}/RECORD +17 -17
- {atex-0.11.dist-info → atex-0.13.dist-info}/WHEEL +0 -0
- {atex-0.11.dist-info → atex-0.13.dist-info}/entry_points.txt +0 -0
- {atex-0.11.dist-info → atex-0.13.dist-info}/licenses/COPYING.txt +0 -0
atex/cli/__init__.py
CHANGED
atex/cli/testingfarm.py
CHANGED
|
@@ -2,7 +2,6 @@ import sys
|
|
|
2
2
|
import json
|
|
3
3
|
import pprint
|
|
4
4
|
import collections
|
|
5
|
-
from datetime import datetime, timedelta, UTC
|
|
6
5
|
|
|
7
6
|
from .. import util
|
|
8
7
|
from ..provisioner.testingfarm import api as tf
|
|
@@ -32,7 +31,8 @@ def composes(args):
|
|
|
32
31
|
comps = api.composes(ranch=args.ranch)
|
|
33
32
|
comps_list = comps["composes"]
|
|
34
33
|
for comp in comps_list:
|
|
35
|
-
|
|
34
|
+
if comp["type"] == "compose":
|
|
35
|
+
print(comp["name"])
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def get_request(args):
|
|
@@ -48,36 +48,63 @@ def cancel(args):
|
|
|
48
48
|
|
|
49
49
|
def search_requests(args):
|
|
50
50
|
api = _get_api(args)
|
|
51
|
-
reply = api.search_requests(
|
|
52
|
-
state=args.state,
|
|
53
|
-
mine=not args.all,
|
|
54
|
-
user_id=args.user_id,
|
|
55
|
-
token_id=args.token_id,
|
|
56
|
-
ranch=args.ranch,
|
|
57
|
-
created_before=args.before,
|
|
58
|
-
created_after=args.after,
|
|
59
|
-
)
|
|
60
|
-
if not reply:
|
|
61
|
-
return
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
func_kwargs = {
|
|
53
|
+
"mine": not args.all,
|
|
54
|
+
"user_id": args.user_id,
|
|
55
|
+
"token_id": args.token_id,
|
|
56
|
+
"ranch": args.ranch,
|
|
57
|
+
"created_before": args.before,
|
|
58
|
+
"created_after": args.after,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if args.page is not None:
|
|
62
|
+
reply = api.search_requests_paged(
|
|
63
|
+
state=args.state,
|
|
64
|
+
page=args.page,
|
|
65
|
+
**func_kwargs,
|
|
66
|
+
)
|
|
67
|
+
if not reply:
|
|
68
|
+
return
|
|
66
69
|
else:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
reply = api.search_requests(
|
|
71
|
+
state=args.state,
|
|
72
|
+
**func_kwargs,
|
|
73
|
+
)
|
|
74
|
+
if not reply:
|
|
75
|
+
return
|
|
76
|
+
reply = sorted(reply, key=lambda x: x["created"])
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
arch = env["arch"]
|
|
76
|
-
if compose and arch:
|
|
77
|
-
envs.append(f"{compose}@{arch}")
|
|
78
|
-
envs_str = ", ".join(envs)
|
|
78
|
+
if args.json:
|
|
79
|
+
for req in reply:
|
|
80
|
+
print(json.dumps(req))
|
|
81
|
+
return
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
for req in reply:
|
|
84
|
+
req_id = req["id"]
|
|
85
|
+
created = req["created"].partition(".")[0]
|
|
86
|
+
|
|
87
|
+
if "fmf" in req["test"] and req["test"]["fmf"]:
|
|
88
|
+
test = req["test"]["fmf"]["url"]
|
|
89
|
+
elif "tmt" in req["test"] and req["test"]["tmt"]:
|
|
90
|
+
test = req["test"]["tmf"]["url"]
|
|
91
|
+
else:
|
|
92
|
+
test = ""
|
|
93
|
+
|
|
94
|
+
envs = []
|
|
95
|
+
for env in req["environments_requested"]:
|
|
96
|
+
if "os" in env and env["os"] and "compose" in env["os"]:
|
|
97
|
+
compose = env["os"]["compose"]
|
|
98
|
+
arch = env["arch"]
|
|
99
|
+
if compose and arch:
|
|
100
|
+
envs.append(f"{compose}@{arch}")
|
|
101
|
+
|
|
102
|
+
print(f"{created} {req_id}", end="")
|
|
103
|
+
if test:
|
|
104
|
+
print(f" | test:{test}", end="")
|
|
105
|
+
if envs:
|
|
106
|
+
print(f" | envs:[{', '.join(envs)}]", end="")
|
|
107
|
+
print()
|
|
81
108
|
|
|
82
109
|
|
|
83
110
|
def stats(args):
|
|
@@ -110,35 +137,35 @@ def stats(args):
|
|
|
110
137
|
print(f"{count:>{digits}} {repo_url}")
|
|
111
138
|
|
|
112
139
|
def request_search_results():
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
yield from result
|
|
121
|
-
|
|
122
|
-
def multiday_request_search_results():
|
|
123
|
-
now = datetime.now(UTC)
|
|
124
|
-
for day in range(0,args.days):
|
|
125
|
-
before = now - timedelta(days=day)
|
|
126
|
-
after = now - timedelta(days=day+1)
|
|
140
|
+
func_kwargs = {}
|
|
141
|
+
if args.before is not None:
|
|
142
|
+
func_kwargs["created_before"] = args.before
|
|
143
|
+
if args.after is not None:
|
|
144
|
+
func_kwargs["created_after"] = args.after
|
|
145
|
+
|
|
146
|
+
if args.page is not None:
|
|
127
147
|
for state in args.states.split(","):
|
|
128
|
-
|
|
148
|
+
reply = api.search_requests_paged(
|
|
129
149
|
state=state,
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
page=args.page,
|
|
151
|
+
mine=False,
|
|
132
152
|
ranch=args.ranch,
|
|
153
|
+
**func_kwargs,
|
|
154
|
+
)
|
|
155
|
+
if reply:
|
|
156
|
+
yield from reply
|
|
157
|
+
else:
|
|
158
|
+
for state in args.states.split(","):
|
|
159
|
+
reply = api.search_requests(
|
|
160
|
+
state=state,
|
|
133
161
|
mine=False,
|
|
162
|
+
ranch=args.ranch,
|
|
163
|
+
**func_kwargs,
|
|
134
164
|
)
|
|
135
|
-
if
|
|
136
|
-
yield from
|
|
165
|
+
if reply:
|
|
166
|
+
yield from reply
|
|
137
167
|
|
|
138
|
-
|
|
139
|
-
top_users_repos(multiday_request_search_results())
|
|
140
|
-
else:
|
|
141
|
-
top_users_repos(request_search_results())
|
|
168
|
+
top_users_repos(request_search_results())
|
|
142
169
|
|
|
143
170
|
|
|
144
171
|
def reserve(args):
|
|
@@ -258,12 +285,15 @@ def parse_args(parser):
|
|
|
258
285
|
cmd.add_argument("--before", help="only requests created before ISO8601")
|
|
259
286
|
cmd.add_argument("--after", help="only requests created after ISO8601")
|
|
260
287
|
cmd.add_argument("--json", help="full details, one request per line", action="store_true")
|
|
288
|
+
cmd.add_argument("--page", help="do paged search, page interval in secs", type=int)
|
|
261
289
|
|
|
262
290
|
cmd = cmds.add_parser(
|
|
263
291
|
"stats",
|
|
264
292
|
help="print out TF usage statistics",
|
|
265
293
|
)
|
|
266
|
-
cmd.add_argument("--
|
|
294
|
+
cmd.add_argument("--before", help="only requests created before ISO8601")
|
|
295
|
+
cmd.add_argument("--after", help="only requests created after ISO8601")
|
|
296
|
+
cmd.add_argument("--page", help="do paged search, page interval in secs", type=int)
|
|
267
297
|
cmd.add_argument("ranch", help="Testing Farm ranch name")
|
|
268
298
|
cmd.add_argument("states", help="comma-separated TF request states")
|
|
269
299
|
|
atex/connection/ssh.py
CHANGED
atex/executor/__init__.py
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
class ExecutorError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Raised by class Executor.
|
|
4
|
+
"""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestSetupError(ExecutorError):
|
|
9
|
+
"""
|
|
10
|
+
Raised when the preparation for test execution (ie. pkg install) fails.
|
|
11
|
+
"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestAbortedError(ExecutorError):
|
|
16
|
+
"""
|
|
17
|
+
Raised when an infrastructure-related issue happened while running a test.
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from . import testcontrol # noqa: F401, E402
|
|
23
|
+
from .executor import Executor # noqa: F401, E402
|
atex/executor/executor.py
CHANGED
|
@@ -8,18 +8,11 @@ import subprocess
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
from .. import util, fmf
|
|
11
|
-
from . import testcontrol, scripts
|
|
11
|
+
from . import TestSetupError, TestAbortedError, testcontrol, scripts
|
|
12
12
|
from .duration import Duration
|
|
13
13
|
from .reporter import Reporter
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class TestAbortedError(Exception):
|
|
17
|
-
"""
|
|
18
|
-
Raised when an infrastructure-related issue happened while running a test.
|
|
19
|
-
"""
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
16
|
class Executor:
|
|
24
17
|
"""
|
|
25
18
|
Logic for running tests on a remote system and processing results
|
|
@@ -222,16 +215,6 @@ class Executor:
|
|
|
222
215
|
output_dir = Path(output_dir)
|
|
223
216
|
test_data = self.fmf_tests.tests[test_name]
|
|
224
217
|
|
|
225
|
-
# run a setup script, preparing wrapper + test scripts
|
|
226
|
-
setup_script = scripts.test_setup(
|
|
227
|
-
test=scripts.Test(test_name, test_data, self.fmf_tests.test_dirs[test_name]),
|
|
228
|
-
tests_dir=self.tests_dir,
|
|
229
|
-
wrapper_exec=f"{self.work_dir}/wrapper.sh",
|
|
230
|
-
test_exec=f"{self.work_dir}/test.sh",
|
|
231
|
-
test_yaml=f"{self.work_dir}/metadata.yaml",
|
|
232
|
-
)
|
|
233
|
-
self.conn.cmd(("bash",), input=setup_script, text=True, check=True)
|
|
234
|
-
|
|
235
218
|
# start with fmf-plan-defined environment
|
|
236
219
|
env_vars = {
|
|
237
220
|
**self.fmf_tests.plan_env,
|
|
@@ -253,6 +236,28 @@ class Executor:
|
|
|
253
236
|
duration = Duration(test_data.get("duration", "5m"))
|
|
254
237
|
control = testcontrol.TestControl(reporter=reporter, duration=duration)
|
|
255
238
|
|
|
239
|
+
# run a setup script, preparing wrapper + test scripts
|
|
240
|
+
setup_script = scripts.test_setup(
|
|
241
|
+
test=scripts.Test(test_name, test_data, self.fmf_tests.test_dirs[test_name]),
|
|
242
|
+
tests_dir=self.tests_dir,
|
|
243
|
+
wrapper_exec=f"{self.work_dir}/wrapper.sh",
|
|
244
|
+
test_exec=f"{self.work_dir}/test.sh",
|
|
245
|
+
test_yaml=f"{self.work_dir}/metadata.yaml",
|
|
246
|
+
)
|
|
247
|
+
setup_proc = self.conn.cmd(
|
|
248
|
+
("bash",),
|
|
249
|
+
input=setup_script,
|
|
250
|
+
stdout=subprocess.PIPE,
|
|
251
|
+
stderr=subprocess.STDOUT,
|
|
252
|
+
text=True,
|
|
253
|
+
)
|
|
254
|
+
if setup_proc.returncode != 0:
|
|
255
|
+
reporter.report({
|
|
256
|
+
"status": "infra",
|
|
257
|
+
"note": f"TestSetupError({setup_proc.stdout})",
|
|
258
|
+
})
|
|
259
|
+
raise TestSetupError(setup_proc.stdout)
|
|
260
|
+
|
|
256
261
|
test_proc = None
|
|
257
262
|
control_fd = None
|
|
258
263
|
stack.callback(lambda: os.close(control_fd) if control_fd else None)
|
atex/executor/reporter.py
CHANGED
|
@@ -85,16 +85,15 @@ class Reporter:
|
|
|
85
85
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
86
|
return file_path
|
|
87
87
|
|
|
88
|
-
def
|
|
88
|
+
def open_fd(self, file_name, mode, result_name=None):
|
|
89
89
|
"""
|
|
90
90
|
Open a file named 'file_name' in a directory relevant to 'result_name'.
|
|
91
|
-
Returns an opened file
|
|
92
|
-
just like with regular open().
|
|
91
|
+
Returns an opened file descriptor that can be closed with os.close().
|
|
93
92
|
|
|
94
93
|
If 'result_name' (typically a subtest) is not given, open the file
|
|
95
94
|
for the test (name) itself.
|
|
96
95
|
"""
|
|
97
|
-
return open(self._dest_path(file_name, result_name), mode)
|
|
96
|
+
return os.open(self._dest_path(file_name, result_name), mode)
|
|
98
97
|
|
|
99
98
|
def link_testout(self, file_name, result_name=None):
|
|
100
99
|
# TODO: docstring
|
atex/executor/scripts.py
CHANGED
|
@@ -86,7 +86,8 @@ def test_wrapper(*, test, tests_dir, test_exec):
|
|
|
86
86
|
out += ")\n"
|
|
87
87
|
|
|
88
88
|
# write test exitcode to test control stream
|
|
89
|
-
|
|
89
|
+
if os.environ.get("ATEX_DEBUG_NO_EXITCODE") != "1":
|
|
90
|
+
out += "echo exitcode $? >&$orig_stdout\n"
|
|
90
91
|
|
|
91
92
|
# always exit the wrapper with 0 if test execution was normal
|
|
92
93
|
out += "exit 0\n"
|
|
@@ -94,17 +95,6 @@ def test_wrapper(*, test, tests_dir, test_exec):
|
|
|
94
95
|
return out
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
def _install_packages(pkgs, extra_opts=None):
|
|
98
|
-
pkgs_str = " ".join(pkgs)
|
|
99
|
-
extra_opts = extra_opts or ()
|
|
100
|
-
dnf = ["dnf", "-y", "--setopt=install_weak_deps=False", "install", *extra_opts]
|
|
101
|
-
dnf_str = " ".join(dnf)
|
|
102
|
-
return util.dedent(fr"""
|
|
103
|
-
not_installed=$(rpm -q --qf '' {pkgs_str} | sed -nr 's/^package ([^ ]+) is not installed$/\1/p')
|
|
104
|
-
[[ $not_installed ]] && {dnf_str} $not_installed
|
|
105
|
-
""") # noqa: E501
|
|
106
|
-
|
|
107
|
-
|
|
108
98
|
def test_setup(*, test, wrapper_exec, test_exec, test_yaml, **kwargs):
|
|
109
99
|
"""
|
|
110
100
|
Generate a bash script that should prepare the remote end for test
|
|
@@ -133,9 +123,19 @@ def test_setup(*, test, wrapper_exec, test_exec, test_yaml, **kwargs):
|
|
|
133
123
|
# install test dependencies
|
|
134
124
|
# - only strings (package names) in require/recommend are supported
|
|
135
125
|
if require := list(fmf.test_pkg_requires(test.data, "require")):
|
|
136
|
-
|
|
126
|
+
pkgs_str = " ".join(require)
|
|
127
|
+
out += util.dedent(fr"""
|
|
128
|
+
not_installed=$(rpm -q --qf '' {pkgs_str} | sed -nr 's/^package ([^ ]+) is not installed$/\1/p')
|
|
129
|
+
[[ $not_installed ]] && dnf -y --setopt=install_weak_deps=False install $not_installed
|
|
130
|
+
""") + "\n" # noqa: E501
|
|
137
131
|
if recommend := list(fmf.test_pkg_requires(test.data, "recommend")):
|
|
138
|
-
|
|
132
|
+
pkgs_str = " ".join(recommend)
|
|
133
|
+
out += util.dedent(fr"""
|
|
134
|
+
have_dnf5=$(command -v dnf5) || true
|
|
135
|
+
skip_bad="--skip-broken${{have_dnf5:+ --skip-unavailable}}"
|
|
136
|
+
not_installed=$(rpm -q --qf '' {pkgs_str} | sed -nr 's/^package ([^ ]+) is not installed$/\1/p')
|
|
137
|
+
[[ $not_installed ]] && dnf -y --setopt=install_weak_deps=False install $skip_bad $not_installed
|
|
138
|
+
""") + "\n" # noqa: E501
|
|
139
139
|
|
|
140
140
|
# write out test data
|
|
141
141
|
out += f"cat > '{test_yaml}' <<'ATEX_SETUP_EOF'\n"
|
atex/executor/testcontrol.py
CHANGED
|
@@ -267,40 +267,45 @@ class TestControl:
|
|
|
267
267
|
except ValueError as e:
|
|
268
268
|
raise BadReportJSONError(f"file entry {file_name} length: {str(e)}") from None
|
|
269
269
|
|
|
270
|
+
fd = self.reporter.open_fd(file_name, os.O_WRONLY | os.O_CREAT, name)
|
|
270
271
|
try:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
# Linux can't do splice(2) on O_APPEND fds, so we open it above
|
|
273
|
+
# as O_WRONLY and just seek to the end, simulating append
|
|
274
|
+
os.lseek(fd, 0, os.SEEK_END)
|
|
275
|
+
|
|
276
|
+
while file_length > 0:
|
|
277
|
+
try:
|
|
278
|
+
# try a more universal sendfile first, fall back to splice
|
|
274
279
|
try:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
raise
|
|
283
|
-
except BlockingIOError:
|
|
284
|
-
yield
|
|
285
|
-
continue
|
|
286
|
-
if written == 0:
|
|
287
|
-
raise BadControlError("EOF when reading data")
|
|
288
|
-
file_length -= written
|
|
280
|
+
written = os.sendfile(fd, self.control_fd, None, file_length)
|
|
281
|
+
except OSError as e:
|
|
282
|
+
if e.errno == 22: # EINVAL
|
|
283
|
+
written = os.splice(self.control_fd, fd, file_length)
|
|
284
|
+
else:
|
|
285
|
+
raise
|
|
286
|
+
except BlockingIOError:
|
|
289
287
|
yield
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
continue
|
|
289
|
+
if written == 0:
|
|
290
|
+
raise BadControlError("EOF when reading data")
|
|
291
|
+
file_length -= written
|
|
292
|
+
yield
|
|
293
|
+
finally:
|
|
294
|
+
os.close(fd)
|
|
292
295
|
|
|
293
296
|
# either store partial result + return,
|
|
294
297
|
# or load previous partial result and merge into it
|
|
295
|
-
partial = result.get("partial"
|
|
296
|
-
if partial:
|
|
297
|
-
# do not store the 'partial' key in the result
|
|
298
|
+
partial = result.get("partial")
|
|
299
|
+
if partial is not None:
|
|
300
|
+
# do not store the 'partial' key in the result, even if False
|
|
298
301
|
del result["partial"]
|
|
299
|
-
#
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
# if it exists and is True
|
|
303
|
+
if partial:
|
|
304
|
+
# note that nameless result will get None as dict key,
|
|
305
|
+
# which is perfectly fine
|
|
306
|
+
self._merge(self.partial_results[name], result)
|
|
307
|
+
# partial = do nothing
|
|
308
|
+
return
|
|
304
309
|
|
|
305
310
|
# if previously-stored partial result exist, merge the current one
|
|
306
311
|
# into it, but then use the merged result
|
atex/orchestrator/adhoc.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import tempfile
|
|
2
|
-
import collections
|
|
3
2
|
import concurrent.futures
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
@@ -60,7 +59,7 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
60
59
|
|
|
61
60
|
def __init__(
|
|
62
61
|
self, platform, fmf_tests, provisioners, aggregator, tmp_dir, *,
|
|
63
|
-
max_remotes=1, max_spares=0,
|
|
62
|
+
max_remotes=1, max_spares=0, max_failed_setups=10, env=None,
|
|
64
63
|
):
|
|
65
64
|
"""
|
|
66
65
|
'platform' is a string with platform name.
|
|
@@ -84,15 +83,15 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
84
83
|
speed up test reruns as Remote reservation happens asynchronously
|
|
85
84
|
to test execution. Spares are reserved on top of 'max_remotes'.
|
|
86
85
|
|
|
87
|
-
'max_reruns' is an integer of how many times to re-try running a failed
|
|
88
|
-
test (which exited with non-0 or caused an Executor exception).
|
|
89
|
-
|
|
90
86
|
'max_failed_setups' is an integer of how many times an Executor's
|
|
91
87
|
plan setup (uploading tests, running prepare scripts, etc.) may fail
|
|
92
88
|
before FailedSetupError is raised.
|
|
93
89
|
|
|
94
90
|
'env' is a dict of extra environment variables to pass to Executor.
|
|
95
91
|
"""
|
|
92
|
+
if not fmf_tests.tests:
|
|
93
|
+
raise ValueError("'fmf_tests' has no tests (bad discover params?)")
|
|
94
|
+
|
|
96
95
|
self.platform = platform
|
|
97
96
|
self.fmf_tests = fmf_tests
|
|
98
97
|
self.provisioners = tuple(provisioners)
|
|
@@ -101,11 +100,11 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
101
100
|
self.failed_setups_left = max_failed_setups
|
|
102
101
|
self.max_remotes = max_remotes
|
|
103
102
|
self.max_spares = max_spares
|
|
104
|
-
# indexed by test name, value being integer of how many times
|
|
105
|
-
self.reruns = collections.defaultdict(lambda: max_reruns)
|
|
106
103
|
self.env = env
|
|
107
104
|
# tests still waiting to be run
|
|
108
105
|
self.to_run = set(fmf_tests.tests)
|
|
106
|
+
# number of Remotes being provisioned + set up (not running tests)
|
|
107
|
+
self.remotes_requested = 0
|
|
109
108
|
# running tests as a dict, indexed by test name, with RunningInfo values
|
|
110
109
|
self.running_tests = {}
|
|
111
110
|
# thread queue for actively running tests
|
|
@@ -159,22 +158,6 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
159
158
|
'finfo' is a FinishedInfo instance.
|
|
160
159
|
"""
|
|
161
160
|
test_data = self.fmf_tests.tests[finfo.test_name]
|
|
162
|
-
|
|
163
|
-
# TODO: somehow move logging from was_successful and should_be_rerun here,
|
|
164
|
-
# probably print just some generic info from those functions that doesn't
|
|
165
|
-
# imply any outcome, ie.
|
|
166
|
-
# {remote_with_test} threw {exception}
|
|
167
|
-
# {remote_with_test} exited with {code}
|
|
168
|
-
# {remote_with_test} has {N} reruns left
|
|
169
|
-
# {remote_with_test} has 0 reruns left
|
|
170
|
-
# and then log the decision separately, here below, such as
|
|
171
|
-
# {remote_with_test} failed, re-running
|
|
172
|
-
# {remote_with_test} completed, ingesting result
|
|
173
|
-
# {remote_with_test} was destructive, releasing remote
|
|
174
|
-
# {remote_with_test} ...., running next test
|
|
175
|
-
# That allows the user to override the functions, while keeping critical
|
|
176
|
-
# flow reliably logged here.
|
|
177
|
-
|
|
178
161
|
remote_with_test = f"{finfo.remote}: '{finfo.test_name}'"
|
|
179
162
|
|
|
180
163
|
if not self.was_successful(finfo, test_data) and self.should_be_rerun(finfo, test_data):
|
|
@@ -215,31 +198,33 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
215
198
|
tmp_dir=None,
|
|
216
199
|
)
|
|
217
200
|
|
|
218
|
-
# if
|
|
219
|
-
#
|
|
220
|
-
if finfo.exception or self.destructive(finfo, test_data):
|
|
221
|
-
util.debug(f"{remote_with_test} was destructive,
|
|
201
|
+
# if there are still tests to be run and the last test was not
|
|
202
|
+
# destructive, just run a new test on it
|
|
203
|
+
if self.to_run and not (finfo.exception or self.destructive(finfo, test_data)):
|
|
204
|
+
util.debug(f"{remote_with_test} was non-destructive, running next test")
|
|
205
|
+
self._run_new_test(finfo)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# we are not running a new test right now, serve_once() might run it
|
|
209
|
+
# some time later, just decide what to do with the current remote
|
|
210
|
+
|
|
211
|
+
if self.remotes_requested >= len(self.to_run):
|
|
212
|
+
# we have enough remotes in the pipe to run every test,
|
|
213
|
+
# we don't need a new one - just release the current one
|
|
214
|
+
util.debug(f"{finfo.remote} no longer useful, releasing it")
|
|
222
215
|
self.release_queue.start_thread(
|
|
223
216
|
finfo.remote.release,
|
|
224
217
|
remote=finfo.remote,
|
|
225
218
|
)
|
|
226
|
-
# TODO: should this be conditioned by 'self.to_run:' ? to not uselessly fall
|
|
227
|
-
# into setup spares and get immediately released after setup?
|
|
228
|
-
finfo.provisioner.provision(1)
|
|
229
|
-
|
|
230
|
-
# if still not destroyed, run another test on it
|
|
231
|
-
# (without running plan setup, re-using already set up remote)
|
|
232
|
-
elif self.to_run:
|
|
233
|
-
util.debug(f"{remote_with_test} was non-destructive, running next test")
|
|
234
|
-
self._run_new_test(finfo)
|
|
235
|
-
|
|
236
|
-
# no more tests to run, release the remote
|
|
237
219
|
else:
|
|
238
|
-
|
|
220
|
+
# we need more remotes and the last test was destructive,
|
|
221
|
+
# get a new one and let serve_once() run a test later
|
|
222
|
+
util.debug(f"{remote_with_test} was destructive, getting a new Remote")
|
|
239
223
|
self.release_queue.start_thread(
|
|
240
224
|
finfo.remote.release,
|
|
241
225
|
remote=finfo.remote,
|
|
242
226
|
)
|
|
227
|
+
finfo.provisioner.provision(1)
|
|
243
228
|
|
|
244
229
|
def serve_once(self):
|
|
245
230
|
"""
|
|
@@ -286,6 +271,7 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
286
271
|
except util.ThreadQueue.Empty:
|
|
287
272
|
break
|
|
288
273
|
|
|
274
|
+
self.remotes_requested -= 1
|
|
289
275
|
sinfo = treturn.sinfo
|
|
290
276
|
|
|
291
277
|
if treturn.exception:
|
|
@@ -295,10 +281,11 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
295
281
|
sinfo.remote.release,
|
|
296
282
|
remote=sinfo.remote,
|
|
297
283
|
)
|
|
298
|
-
if (
|
|
299
|
-
util.warning(f"{msg}, re-trying ({
|
|
284
|
+
if (retries_left := self.failed_setups_left) > 0:
|
|
285
|
+
util.warning(f"{msg}, re-trying ({retries_left} setup retries left)")
|
|
300
286
|
self.failed_setups_left -= 1
|
|
301
287
|
sinfo.provisioner.provision(1)
|
|
288
|
+
self.remotes_requested += 1
|
|
302
289
|
else:
|
|
303
290
|
util.warning(f"{msg}, setup retries exceeded, giving up")
|
|
304
291
|
raise FailedSetupError("setup retries limit exceeded, broken infra?")
|
|
@@ -318,8 +305,9 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
318
305
|
treturn.sinfo.remote.release,
|
|
319
306
|
remote=treturn.sinfo.remote,
|
|
320
307
|
)
|
|
308
|
+
self.remotes_requested -= 1
|
|
321
309
|
|
|
322
|
-
# try to get new
|
|
310
|
+
# try to get new Remotes from Provisioners - if we get some, start
|
|
323
311
|
# running setup on them
|
|
324
312
|
for provisioner in self.provisioners:
|
|
325
313
|
while (remote := provisioner.get_remote(block=False)) is not None:
|
|
@@ -371,9 +359,12 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
371
359
|
for prov in self.provisioners:
|
|
372
360
|
prov.start()
|
|
373
361
|
|
|
362
|
+
# just the base remotes, no spares
|
|
363
|
+
self.remotes_requested = min(self.max_remotes, len(self.fmf_tests.tests))
|
|
364
|
+
|
|
374
365
|
# start up initial reservations, balanced evenly across all available
|
|
375
366
|
# provisioner instances
|
|
376
|
-
count =
|
|
367
|
+
count = self.remotes_requested + self.max_spares
|
|
377
368
|
provisioners = self.provisioners[:count]
|
|
378
369
|
for idx, prov in enumerate(provisioners):
|
|
379
370
|
if count % len(provisioners) > idx:
|
|
@@ -480,24 +471,20 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
480
471
|
'test_data' is a dict of fully resolved fmf test metadata of that test.
|
|
481
472
|
"""
|
|
482
473
|
remote_with_test = f"{info.remote}: '{info.test_name}'"
|
|
483
|
-
|
|
484
474
|
# executor (or test) threw exception
|
|
485
475
|
if info.exception:
|
|
486
476
|
exc_str = f"{type(info.exception).__name__}({info.exception})"
|
|
487
477
|
util.info(f"{remote_with_test} threw {exc_str} during test runtime")
|
|
488
478
|
return False
|
|
489
|
-
|
|
490
479
|
# the test exited as non-0
|
|
491
480
|
if info.exit_code != 0:
|
|
492
481
|
util.info(f"{remote_with_test} exited with non-zero: {info.exit_code}")
|
|
493
482
|
return False
|
|
494
|
-
|
|
495
483
|
# otherwise we good
|
|
496
484
|
return True
|
|
497
485
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
def should_be_rerun(self, info, test_data): # noqa: ARG004, ARG002
|
|
486
|
+
@staticmethod
|
|
487
|
+
def should_be_rerun(info, test_data): # noqa: ARG004
|
|
501
488
|
"""
|
|
502
489
|
Return a boolean result whether a finished test failed in a way
|
|
503
490
|
that another execution attempt might succeed, due to race conditions
|
|
@@ -507,17 +494,5 @@ class AdHocOrchestrator(Orchestrator):
|
|
|
507
494
|
|
|
508
495
|
'test_data' is a dict of fully resolved fmf test metadata of that test.
|
|
509
496
|
"""
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
# TODO: remove self.reruns and the whole X-reruns logic from AdHocOrchestrator,
|
|
513
|
-
# leave it up to the user to wrap should_be_rerun() with an external dict
|
|
514
|
-
# of tests, counting reruns for each
|
|
515
|
-
# - allows the user to adjust counts per-test (ie. test_data metadata)
|
|
516
|
-
# - allows this template to be @staticmethod
|
|
517
|
-
reruns_left = self.reruns[info.test_name]
|
|
518
|
-
util.info(f"{remote_with_test}: {reruns_left} reruns left")
|
|
519
|
-
if reruns_left > 0:
|
|
520
|
-
self.reruns[info.test_name] -= 1
|
|
521
|
-
return True
|
|
522
|
-
else:
|
|
523
|
-
return False
|
|
497
|
+
# never rerun by default
|
|
498
|
+
return False
|
atex/orchestrator/contest.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
|
|
1
3
|
from .. import util
|
|
2
4
|
from .adhoc import AdHocOrchestrator
|
|
3
5
|
|
|
@@ -26,9 +28,18 @@ class ContestOrchestrator(AdHocOrchestrator):
|
|
|
26
28
|
"""
|
|
27
29
|
content_dir_on_remote = "/root/upstream-content"
|
|
28
30
|
|
|
29
|
-
def __init__(self, *args, content_dir, **kwargs):
|
|
30
|
-
|
|
31
|
+
def __init__(self, *args, content_dir, max_reruns=1, **kwargs):
|
|
32
|
+
"""
|
|
33
|
+
'content_dir' is a filesystem path to ComplianceAsCode/content local
|
|
34
|
+
directory, to be uploaded to the tested systems.
|
|
35
|
+
|
|
36
|
+
'max_reruns' is an integer of how many times to re-try running a failed
|
|
37
|
+
test (which exited with non-0 or caused an Executor exception).
|
|
38
|
+
"""
|
|
31
39
|
super().__init__(*args, **kwargs)
|
|
40
|
+
self.content_dir = content_dir
|
|
41
|
+
# indexed by test name, value being integer of how many times
|
|
42
|
+
self.reruns = collections.defaultdict(lambda: max_reruns)
|
|
32
43
|
|
|
33
44
|
def run_setup(self, sinfo):
|
|
34
45
|
super().run_setup(sinfo)
|
|
@@ -72,8 +83,8 @@ class ContestOrchestrator(AdHocOrchestrator):
|
|
|
72
83
|
# fallback to the default next_test()
|
|
73
84
|
return super().next_test(to_run, all_tests, previous)
|
|
74
85
|
|
|
75
|
-
@
|
|
76
|
-
def destructive(
|
|
86
|
+
@staticmethod
|
|
87
|
+
def destructive(info, test_data):
|
|
77
88
|
# if Executor ended with an exception (ie. duration exceeded),
|
|
78
89
|
# consider the test destructive
|
|
79
90
|
if info.exception:
|
|
@@ -92,3 +103,14 @@ class ContestOrchestrator(AdHocOrchestrator):
|
|
|
92
103
|
return True
|
|
93
104
|
|
|
94
105
|
return False
|
|
106
|
+
|
|
107
|
+
def should_be_rerun(self, info, test_data): # noqa: ARG004, ARG002
|
|
108
|
+
remote_with_test = f"{info.remote}: '{info.test_name}'"
|
|
109
|
+
|
|
110
|
+
reruns_left = self.reruns[info.test_name]
|
|
111
|
+
util.info(f"{remote_with_test}: {reruns_left} reruns left")
|
|
112
|
+
if reruns_left > 0:
|
|
113
|
+
self.reruns[info.test_name] -= 1
|
|
114
|
+
return True
|
|
115
|
+
else:
|
|
116
|
+
return False
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
import time
|
|
4
4
|
import tempfile
|
|
5
|
+
import datetime
|
|
5
6
|
import textwrap
|
|
6
7
|
import threading
|
|
7
8
|
import subprocess
|
|
@@ -14,11 +15,11 @@ from ... import util
|
|
|
14
15
|
import json
|
|
15
16
|
import urllib3
|
|
16
17
|
|
|
17
|
-
DEFAULT_API_URL = "https://api.testing-farm.io
|
|
18
|
+
DEFAULT_API_URL = "https://api.testing-farm.io"
|
|
18
19
|
|
|
19
20
|
DEFAULT_RESERVE_TEST = {
|
|
20
21
|
"url": "https://github.com/RHSecurityCompliance/atex-reserve",
|
|
21
|
-
"ref": "
|
|
22
|
+
"ref": "main",
|
|
22
23
|
"path": ".",
|
|
23
24
|
"name": "/plans/reserve",
|
|
24
25
|
}
|
|
@@ -85,8 +86,8 @@ class TestingFarmAPI:
|
|
|
85
86
|
self.api_url = url
|
|
86
87
|
self.api_token = token or os.environ.get("TESTING_FARM_API_TOKEN")
|
|
87
88
|
|
|
88
|
-
def _query(self, method, path, *args, headers=None, auth=True, **kwargs):
|
|
89
|
-
url = f"{self.api_url}{path}"
|
|
89
|
+
def _query(self, method, path, *args, headers=None, auth=True, version="v0.1", **kwargs):
|
|
90
|
+
url = f"{self.api_url}/{version}{path}"
|
|
90
91
|
if self.api_token and auth:
|
|
91
92
|
if headers is not None:
|
|
92
93
|
headers["Authorization"] = f"Bearer {self.api_token}"
|
|
@@ -137,7 +138,7 @@ class TestingFarmAPI:
|
|
|
137
138
|
if not self.api_token:
|
|
138
139
|
raise ValueError("composes() requires an auth token to identify ranch")
|
|
139
140
|
ranch = self.whoami()["token"]["ranch"]
|
|
140
|
-
return self._query("GET", f"/composes/{ranch}")
|
|
141
|
+
return self._query("GET", f"/composes/{ranch}", version="v0.2")
|
|
141
142
|
|
|
142
143
|
def search_requests(
|
|
143
144
|
self, *, state, ranch=None,
|
|
@@ -180,6 +181,52 @@ class TestingFarmAPI:
|
|
|
180
181
|
|
|
181
182
|
return self._query("GET", "/requests", fields=fields, auth=mine)
|
|
182
183
|
|
|
184
|
+
def search_requests_paged(self, *args, page=43200, **kwargs):
|
|
185
|
+
"""
|
|
186
|
+
An unofficial wrapper for search_requests() that can search a large
|
|
187
|
+
interval incrementally (in "pages") and yield batches of results.
|
|
188
|
+
|
|
189
|
+
Needs 'created_after', with 'created_before' defaulting to now().
|
|
190
|
+
|
|
191
|
+
'page' specifies the time interval of one page, in seconds.
|
|
192
|
+
|
|
193
|
+
'args' and 'kwargs' are passed to search_requests().
|
|
194
|
+
"""
|
|
195
|
+
assert "created_after" in kwargs, "at least 'created_after' is needed for paging"
|
|
196
|
+
|
|
197
|
+
def from_iso8601(date):
|
|
198
|
+
dt = datetime.datetime.fromisoformat(date)
|
|
199
|
+
# if no TZ is specified, treat it as UTC, not localtime
|
|
200
|
+
if dt.tzinfo is None:
|
|
201
|
+
dt = dt.replace(tzinfo=datetime.UTC)
|
|
202
|
+
# convert to UTC
|
|
203
|
+
else:
|
|
204
|
+
dt = dt.astimezone(datetime.UTC)
|
|
205
|
+
return dt
|
|
206
|
+
|
|
207
|
+
after = from_iso8601(kwargs["created_after"])
|
|
208
|
+
if kwargs.get("created_before"):
|
|
209
|
+
before = from_iso8601(kwargs["created_before"])
|
|
210
|
+
else:
|
|
211
|
+
before = datetime.datetime.now(datetime.UTC)
|
|
212
|
+
|
|
213
|
+
# scale down page size to fit between after/before
|
|
214
|
+
page = min(page, (before - after).total_seconds())
|
|
215
|
+
|
|
216
|
+
start = after
|
|
217
|
+
while start < before:
|
|
218
|
+
end = start + datetime.timedelta(seconds=page)
|
|
219
|
+
# clamp to real 'before'
|
|
220
|
+
end = min(end, before)
|
|
221
|
+
new_kwargs = kwargs | {
|
|
222
|
+
"created_after": start.isoformat(),
|
|
223
|
+
"created_before": end.isoformat(),
|
|
224
|
+
}
|
|
225
|
+
found = self.search_requests(*args, **new_kwargs)
|
|
226
|
+
if found is not None:
|
|
227
|
+
yield from found
|
|
228
|
+
start = end
|
|
229
|
+
|
|
183
230
|
def get_request(self, request_id):
|
|
184
231
|
"""
|
|
185
232
|
'request_id' is the UUID (string) of the request.
|
|
@@ -389,7 +436,7 @@ class Reserve:
|
|
|
389
436
|
def __init__(
|
|
390
437
|
self, *, compose, arch="x86_64", pool=None, hardware=None, kickstart=None,
|
|
391
438
|
timeout=60, ssh_key=None, source_host=None,
|
|
392
|
-
reserve_test=None, variables=None, secrets=None,
|
|
439
|
+
reserve_test=None, variables=None, secrets=None, tags=None,
|
|
393
440
|
api=None,
|
|
394
441
|
):
|
|
395
442
|
"""
|
|
@@ -435,6 +482,10 @@ class Reserve:
|
|
|
435
482
|
exported for the reserve test - variables are visible via TF API,
|
|
436
483
|
secrets are not (but can still be extracted from pipeline log).
|
|
437
484
|
|
|
485
|
+
'tags' is a dict of custom key/values to be submitted in TF Request as
|
|
486
|
+
environments->settings->provisioning->tags, useful for storing custom
|
|
487
|
+
metadata to be queried later.
|
|
488
|
+
|
|
438
489
|
'api' is a TestingFarmAPI instance - if unspecified, a sensible default
|
|
439
490
|
will be used.
|
|
440
491
|
"""
|
|
@@ -474,6 +525,8 @@ class Reserve:
|
|
|
474
525
|
if variables:
|
|
475
526
|
spec_env["variables"] = variables
|
|
476
527
|
spec_env["secrets"] = secrets.copy() if secrets else {} # we need it for ssh pubkey
|
|
528
|
+
if tags:
|
|
529
|
+
spec_env["settings"]["provisioning"]["tags"] |= tags
|
|
477
530
|
|
|
478
531
|
self._spec = spec
|
|
479
532
|
self._ssh_key = Path(ssh_key) if ssh_key else None
|
|
@@ -490,7 +543,7 @@ class Reserve:
|
|
|
490
543
|
try:
|
|
491
544
|
r = _http.request("GET", "https://ifconfig.me", headers=curl_agent)
|
|
492
545
|
if r.status != 200:
|
|
493
|
-
raise ConnectionError
|
|
546
|
+
raise ConnectionError
|
|
494
547
|
except (ConnectionError, urllib3.exceptions.RequestError):
|
|
495
548
|
r = _http.request("GET", "https://ifconfig.co", headers=curl_agent)
|
|
496
549
|
return r.data.decode().strip()
|
|
@@ -570,7 +623,8 @@ class Reserve:
|
|
|
570
623
|
# installs our ssh pubkey into authorized_keys)
|
|
571
624
|
ssh_attempt_cmd = (
|
|
572
625
|
"ssh", "-q", "-i", ssh_key.absolute(), "-oConnectionAttempts=60",
|
|
573
|
-
|
|
626
|
+
"-oStrictHostKeyChecking=no", "-oUserKnownHostsFile=/dev/null",
|
|
627
|
+
"-oBatchMode=yes",
|
|
574
628
|
f"{ssh_user}@{ssh_host}", "exit 123",
|
|
575
629
|
)
|
|
576
630
|
while True:
|
atex/util/log.py
CHANGED
|
@@ -30,7 +30,7 @@ skip_levels = {
|
|
|
30
30
|
|
|
31
31
|
def _log_msg(logger_func, *args, stacklevel=1, **kwargs):
|
|
32
32
|
# inspect.stack() is MUCH slower
|
|
33
|
-
caller = inspect.currentframe().f_back.f_back
|
|
33
|
+
caller = inspect.currentframe().f_back.f_back # TODO: sys._getframe(2)
|
|
34
34
|
extra_levels = 2 # skip this func and the debug/info/warning parent
|
|
35
35
|
while caller.f_back:
|
|
36
36
|
code = caller.f_code
|
|
@@ -2,22 +2,22 @@ atex/__init__.py,sha256=LdX67gprtHYeAkjLhFPKzpc7ECv2rHxUbHKDGbGXO1c,517
|
|
|
2
2
|
atex/fmf.py,sha256=gkJXIaRO7_KvwJR-V6Tc1NVn4a9Hq7hoBLQLhxYIdbg,8834
|
|
3
3
|
atex/aggregator/__init__.py,sha256=8mN-glHdzR4icKAUGO4JPodsTrLMdJoeuZsO2CTbhyU,1773
|
|
4
4
|
atex/aggregator/json.py,sha256=tpoUZoZM8EMYhZKwVr4LRtgEIDjRxC11BIKVXZKYPOs,10441
|
|
5
|
-
atex/cli/__init__.py,sha256=
|
|
5
|
+
atex/cli/__init__.py,sha256=0YzZ4sIYvZ47yF9STA2znINIQfEoGAV1N0pduTA7NhI,2897
|
|
6
6
|
atex/cli/fmf.py,sha256=pvj_OIp6XT_nVUwziL7-v_HNbyAtuUmb7k_Ey_KkFJc,3616
|
|
7
7
|
atex/cli/libvirt.py,sha256=6tt5ANb8XBBRXOQsYPTWILThKqf-gvt5AZh5Dctg2PA,3782
|
|
8
|
-
atex/cli/testingfarm.py,sha256=
|
|
8
|
+
atex/cli/testingfarm.py,sha256=b6IzoouYx-Qzuu7350QhyC-PW8576b7hyhiTDka_xak,10899
|
|
9
9
|
atex/connection/__init__.py,sha256=dj8ZBcEspom7Z_UjecfLGBRNvLZ3dyGR9q19i_B4xpY,3880
|
|
10
10
|
atex/connection/podman.py,sha256=1T56gh1TgbcQWpTIJHL4NaxZOI6aMg7Xp7sn6PQQyBk,1911
|
|
11
|
-
atex/connection/ssh.py,sha256=
|
|
12
|
-
atex/executor/__init__.py,sha256=
|
|
11
|
+
atex/connection/ssh.py,sha256=6lama8q3tSQcWPRKN3B4lgFUGtuVA51tuKDiNh7IN0U,13484
|
|
12
|
+
atex/executor/__init__.py,sha256=nmYJCbC36fRGGkjoniFJmsq-sqFw8YS2ndf4q_loVM0,471
|
|
13
13
|
atex/executor/duration.py,sha256=x06sItKOZi6XA8KszQwZGpIb1Z_L-HWqIwZKo2SDo0s,1759
|
|
14
|
-
atex/executor/executor.py,sha256=
|
|
15
|
-
atex/executor/reporter.py,sha256=
|
|
16
|
-
atex/executor/scripts.py,sha256=
|
|
17
|
-
atex/executor/testcontrol.py,sha256=
|
|
14
|
+
atex/executor/executor.py,sha256=WJXPWQo6VQhZgXORVVyvTDAdOQbbZz26E7FpwizbGIk,16126
|
|
15
|
+
atex/executor/reporter.py,sha256=QbzBkaXuhI6lsTYrTlp7O5W9d6etR0KjDdH-J59cXWM,3357
|
|
16
|
+
atex/executor/scripts.py,sha256=1u5ZEGJ7nIvkqbRK3uVusOkineVM8DXo4kAlH2MdQbg,5877
|
|
17
|
+
atex/executor/testcontrol.py,sha256=iju_Cl32D8NHH1ePN1lykR1noP8-0eBDLQ5-V_9DqF0,12834
|
|
18
18
|
atex/orchestrator/__init__.py,sha256=8Q1YknyibilXLjWRYkHm_Mr2HMm0SRw8Zv39KypeASM,2059
|
|
19
|
-
atex/orchestrator/adhoc.py,sha256=
|
|
20
|
-
atex/orchestrator/contest.py,sha256=
|
|
19
|
+
atex/orchestrator/adhoc.py,sha256=VUwHX71Vb6eRLzW3Z3KDZdck7p0PiwzAZrOuUKMkwtM,19667
|
|
20
|
+
atex/orchestrator/contest.py,sha256=SuxT9uZtcs_DEsA3hHyKgrIWNrDeqCCWd3-hy3sHytY,4572
|
|
21
21
|
atex/provisioner/__init__.py,sha256=6hZxQlvTQ0yWWqCRCPqWMoYuim5wDMCcDIYHF-nIfMs,4013
|
|
22
22
|
atex/provisioner/libvirt/VM_PROVISION,sha256=7pkZ-ozgTyK4qNGC-E-HUznr4IhbosWSASbB72Gknl8,2664
|
|
23
23
|
atex/provisioner/libvirt/__init__.py,sha256=pKG5IpZSC2IHs5wL2ecQx_fd9AzAXEbZmDzA7RyZsfM,119
|
|
@@ -27,19 +27,19 @@ atex/provisioner/libvirt/setup-libvirt.sh,sha256=oCMy9SCnbC_QuAzO2sFwvB5ui1kMQ6u
|
|
|
27
27
|
atex/provisioner/podman/__init__.py,sha256=dM0JzQXWX7edtWSc0KH0cMFXAjArFn2Vme4j_ZMsdYA,138
|
|
28
28
|
atex/provisioner/podman/podman.py,sha256=ztRypoakSf-jF04iER58tEMUZ4Y6AuzIpNpFXp44bB4,4997
|
|
29
29
|
atex/provisioner/testingfarm/__init__.py,sha256=kZncgLGdRCR4FMaRQr2GTwJ8vjlA-24ri8JO2ueZJuw,113
|
|
30
|
-
atex/provisioner/testingfarm/api.py,sha256=
|
|
30
|
+
atex/provisioner/testingfarm/api.py,sha256=3vPpAmPisn99-NruZHUy_lS0I7A_IKHWGVGJcJBLBuI,23836
|
|
31
31
|
atex/provisioner/testingfarm/testingfarm.py,sha256=yvQzWat92B4UnJNZzCLI8mpAKf_QvHUKyKbjlk5123Q,8573
|
|
32
32
|
atex/util/__init__.py,sha256=cWHFbtQ4mDlKe6lXyPDWRmWJOTcHDGfVuW_-GYa8hB0,1473
|
|
33
33
|
atex/util/dedent.py,sha256=SEuJMtLzqz3dQ7g7qyZzEJ9VYynVlk52tQCJY-FveXo,603
|
|
34
34
|
atex/util/libvirt.py,sha256=kDZmT6xLYEZkQNLZY98gJ2M48DDWXxHF8rQY9PnjB3U,660
|
|
35
|
-
atex/util/log.py,sha256=
|
|
35
|
+
atex/util/log.py,sha256=GfdbLtpRkQoIkRU7AqWDWbJV7yZIpS4MsXhUomZqWjQ,2256
|
|
36
36
|
atex/util/named_mapping.py,sha256=UBMe9TetjV-DGPhjYjJ42YtC40FVPKAAEROXl9MA5fo,4700
|
|
37
37
|
atex/util/path.py,sha256=x-kXqiWCVodfZWbEwtC5A8LFvutpDIPYv2m0boZSlXU,504
|
|
38
38
|
atex/util/ssh_keygen.py,sha256=9yuSl2yBV7pG3Qfsf9tossVC00nbIUrAeLdbwTykpjk,384
|
|
39
39
|
atex/util/subprocess.py,sha256=_oQN8CNgGoH9GAR6nZlpujYe2HjXFBcCuIkLPw-IxJ4,2971
|
|
40
40
|
atex/util/threads.py,sha256=c8hsEc-8SqJGodInorv_6JxpiHiSkGFGob4qbMmOD2M,3531
|
|
41
|
-
atex-0.
|
|
42
|
-
atex-0.
|
|
43
|
-
atex-0.
|
|
44
|
-
atex-0.
|
|
45
|
-
atex-0.
|
|
41
|
+
atex-0.13.dist-info/METADATA,sha256=Q3S7eSPkLtzLYvRgIatm_RYyPGXUa_K7dAqZ_GvV5rY,3050
|
|
42
|
+
atex-0.13.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
43
|
+
atex-0.13.dist-info/entry_points.txt,sha256=pLqJdcfeyQTgup2h6dWb6SvkHhtOl-W5Eg9zV8moK0o,39
|
|
44
|
+
atex-0.13.dist-info/licenses/COPYING.txt,sha256=oEuj51jdmbXcCUy7pZ-KE0BNcJTR1okudRp5zQ0yWnU,670
|
|
45
|
+
atex-0.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|