atex 0.4__py3-none-any.whl → 0.7__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/__init__.py +2 -12
- atex/cli/__init__.py +13 -13
- atex/cli/minitmt.py +128 -35
- atex/cli/testingfarm.py +59 -59
- atex/connection/__init__.py +125 -0
- atex/connection/ssh.py +406 -0
- atex/minitmt/__init__.py +17 -109
- atex/minitmt/executor.py +348 -0
- atex/minitmt/fmf.py +87 -53
- atex/minitmt/scripts.py +143 -45
- atex/minitmt/testcontrol.py +354 -0
- atex/{orchestrator.py → orchestrator/__init__.py} +22 -1
- atex/orchestrator/aggregator.py +163 -0
- atex/provision/__init__.py +77 -35
- atex/provision/libvirt/VM_PROVISION +8 -0
- atex/provision/libvirt/__init__.py +4 -4
- atex/provision/nspawn/README +74 -0
- atex/provision/podman/README +59 -0
- atex/provision/podman/host_container.sh +74 -0
- atex/provision/testingfarm/__init__.py +29 -0
- atex/{testingfarm.py → provision/testingfarm/api.py} +116 -93
- atex/provision/testingfarm/foo.py +1 -0
- atex/util/__init__.py +4 -4
- atex/util/dedent.py +1 -1
- atex/util/log.py +12 -12
- atex/util/subprocess.py +14 -13
- {atex-0.4.dist-info → atex-0.7.dist-info}/METADATA +1 -1
- atex-0.7.dist-info/RECORD +32 -0
- atex/minitmt/report.py +0 -174
- atex/minitmt/testme.py +0 -3
- atex/ssh.py +0 -320
- atex/util/lockable_class.py +0 -38
- atex-0.4.dist-info/RECORD +0 -26
- {atex-0.4.dist-info → atex-0.7.dist-info}/WHEEL +0 -0
- {atex-0.4.dist-info → atex-0.7.dist-info}/entry_points.txt +0 -0
- {atex-0.4.dist-info → atex-0.7.dist-info}/licenses/COPYING.txt +0 -0
atex/__init__.py
CHANGED
|
@@ -18,18 +18,8 @@ def __dir__():
|
|
|
18
18
|
|
|
19
19
|
# lazily import submodules
|
|
20
20
|
def __getattr__(attr):
|
|
21
|
-
# # from mod import *
|
|
22
|
-
# if attr == '__all__':
|
|
23
|
-
# print("importing all")
|
|
24
|
-
# for mod in __all__:
|
|
25
|
-
# _importlib.import_module(f'.{mod}', __name__)
|
|
26
|
-
# return __all__
|
|
27
|
-
# # accessing __all__, __getattr__, etc. directly
|
|
28
|
-
# elif attr in globals():
|
|
29
|
-
# print("importing globals")
|
|
30
|
-
# return globals()[attr]
|
|
31
21
|
# importing a module known to exist
|
|
32
22
|
if attr in __all__:
|
|
33
|
-
return _importlib.import_module(f
|
|
23
|
+
return _importlib.import_module(f".{attr}", __name__)
|
|
34
24
|
else:
|
|
35
|
-
raise AttributeError(f
|
|
25
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{attr}'")
|
atex/cli/__init__.py
CHANGED
|
@@ -32,16 +32,16 @@ def setup_logging(level):
|
|
|
32
32
|
logging.basicConfig(
|
|
33
33
|
level=level,
|
|
34
34
|
stream=sys.stderr,
|
|
35
|
-
format=
|
|
36
|
-
datefmt=
|
|
35
|
+
format="%(asctime)s %(name)s: %(message)s",
|
|
36
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def collect_modules():
|
|
41
41
|
for info in pkgutil.iter_modules(__spec__.submodule_search_locations):
|
|
42
|
-
mod = importlib.import_module(f
|
|
43
|
-
if not hasattr(mod,
|
|
44
|
-
raise ValueError(f"CLI submodule {info.name} does not define CLI_SPEC")
|
|
42
|
+
mod = importlib.import_module(f".{info.name}", __name__)
|
|
43
|
+
if not hasattr(mod, "CLI_SPEC"):
|
|
44
|
+
raise ValueError(f"CLI submodule '{info.name}' does not define CLI_SPEC")
|
|
45
45
|
yield (info.name, mod.CLI_SPEC)
|
|
46
46
|
|
|
47
47
|
|
|
@@ -50,28 +50,28 @@ def main():
|
|
|
50
50
|
|
|
51
51
|
log_grp = parser.add_mutually_exclusive_group()
|
|
52
52
|
log_grp.add_argument(
|
|
53
|
-
|
|
53
|
+
"--debug", "-d", action="store_const", dest="loglevel", const=logging.DEBUG,
|
|
54
54
|
help="enable extra debugging (logging.DEBUG)",
|
|
55
55
|
)
|
|
56
56
|
log_grp.add_argument(
|
|
57
|
-
|
|
57
|
+
"--quiet", "-q", action="store_const", dest="loglevel", const=logging.WARNING,
|
|
58
58
|
help="be quiet during normal operation (logging.WARNING)",
|
|
59
59
|
)
|
|
60
60
|
parser.set_defaults(loglevel=logging.INFO)
|
|
61
61
|
|
|
62
62
|
mains = {}
|
|
63
|
-
subparsers = parser.add_subparsers(dest=
|
|
63
|
+
subparsers = parser.add_subparsers(dest="_module", metavar="<module>", required=True)
|
|
64
64
|
for name, spec in collect_modules():
|
|
65
|
-
aliases = spec[
|
|
65
|
+
aliases = spec["aliases"] if "aliases" in spec else ()
|
|
66
66
|
subp = subparsers.add_parser(
|
|
67
67
|
name,
|
|
68
68
|
aliases=aliases,
|
|
69
|
-
help=spec[
|
|
69
|
+
help=spec["help"],
|
|
70
70
|
)
|
|
71
|
-
spec[
|
|
72
|
-
mains[name] = spec[
|
|
71
|
+
spec["args"](subp)
|
|
72
|
+
mains[name] = spec["main"]
|
|
73
73
|
for alias in aliases:
|
|
74
|
-
mains[alias] = spec[
|
|
74
|
+
mains[alias] = spec["main"]
|
|
75
75
|
|
|
76
76
|
args = parser.parse_args()
|
|
77
77
|
|
atex/cli/minitmt.py
CHANGED
|
@@ -1,82 +1,175 @@
|
|
|
1
|
-
import
|
|
1
|
+
import sys
|
|
2
|
+
#import re
|
|
2
3
|
import pprint
|
|
4
|
+
#import subprocess
|
|
5
|
+
from pathlib import Path
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
from ..
|
|
7
|
+
from .. import connection, provision, minitmt
|
|
8
|
+
from ..orchestrator import aggregator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fatal(msg):
|
|
12
|
+
print(msg, file=sys.stderr)
|
|
13
|
+
sys.exit(1)
|
|
6
14
|
|
|
7
15
|
|
|
8
16
|
def _get_context(args):
|
|
9
17
|
context = {}
|
|
10
18
|
if args.context:
|
|
11
19
|
for c in args.context:
|
|
12
|
-
key, value = c.split(
|
|
20
|
+
key, value = c.split("=", 1)
|
|
13
21
|
context[key] = value
|
|
14
22
|
return context or None
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
def discover(args):
|
|
18
|
-
result = fmf.
|
|
19
|
-
for
|
|
20
|
-
print(
|
|
26
|
+
result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
|
|
27
|
+
for name in result.tests:
|
|
28
|
+
print(name)
|
|
21
29
|
|
|
22
30
|
|
|
23
31
|
def show(args):
|
|
24
|
-
result = fmf.
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
|
|
33
|
+
if tests := list(result.match(args.test)):
|
|
34
|
+
for test in tests:
|
|
35
|
+
print(f"\n--- {test.name} ---")
|
|
27
36
|
pprint.pprint(test.data)
|
|
28
|
-
break
|
|
29
37
|
else:
|
|
38
|
+
_fatal(f"Not reachable via {args.plan} discovery: {args.test}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def execute(args):
|
|
42
|
+
# remote system connection
|
|
43
|
+
ssh_keypath = Path(args.ssh_identity)
|
|
44
|
+
if not ssh_keypath.exists():
|
|
45
|
+
_fatal(f"SSH Identity {args.ssh_identity} does not exist")
|
|
46
|
+
ssh_options = {
|
|
47
|
+
"User": args.user,
|
|
48
|
+
"Hostname": args.host,
|
|
49
|
+
"IdentityFile": ssh_keypath,
|
|
50
|
+
}
|
|
51
|
+
env = dict(x.split("=",1) for x in args.env)
|
|
52
|
+
|
|
53
|
+
# dummy Remote that just wraps the connection
|
|
54
|
+
class DummyRemote(provision.Remote, connection.ssh.ManagedSSHConn):
|
|
55
|
+
@staticmethod
|
|
56
|
+
def release():
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def alive():
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
# result aggregation
|
|
64
|
+
with aggregator.CSVAggregator(args.results_csv, args.results_dir) as csv_aggregator:
|
|
65
|
+
platform_aggregator = csv_aggregator.for_platform(args.platform)
|
|
66
|
+
|
|
67
|
+
# tests discovery and selection
|
|
68
|
+
result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
|
|
69
|
+
if args.test:
|
|
70
|
+
tests = list(result.match(args.test))
|
|
71
|
+
if not tests:
|
|
72
|
+
_fatal(f"Not reachable via plan {args.plan} discovery: {args.test}")
|
|
73
|
+
else:
|
|
74
|
+
tests = list(result.as_fmftests())
|
|
75
|
+
if not tests:
|
|
76
|
+
_fatal(f"No tests found for plan {args.plan}")
|
|
77
|
+
|
|
78
|
+
# test run
|
|
79
|
+
with DummyRemote(ssh_options) as remote:
|
|
80
|
+
executor = minitmt.executor.Executor(remote, platform_aggregator, env=env)
|
|
81
|
+
executor.upload_tests(args.root)
|
|
82
|
+
executor.setup_plan(result)
|
|
83
|
+
for test in tests:
|
|
84
|
+
executor.run_test(test)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def setup_script(args):
|
|
88
|
+
result = minitmt.fmf.FMFTests(args.root, args.plan, context=_get_context(args))
|
|
89
|
+
try:
|
|
90
|
+
test = result.as_fmftest(args.test)
|
|
91
|
+
except KeyError:
|
|
30
92
|
print(f"Not reachable via {args.plan} discovery: {args.test}")
|
|
31
|
-
raise SystemExit(1)
|
|
93
|
+
raise SystemExit(1) from None
|
|
94
|
+
output = minitmt.scripts.test_setup(
|
|
95
|
+
test=test,
|
|
96
|
+
tests_dir=args.remote_root,
|
|
97
|
+
debug=args.script_debug,
|
|
98
|
+
)
|
|
99
|
+
print(output, end="")
|
|
32
100
|
|
|
33
101
|
|
|
34
102
|
def parse_args(parser):
|
|
35
|
-
parser.add_argument(
|
|
36
|
-
parser.add_argument(
|
|
103
|
+
parser.add_argument("--root", help="path to directory with fmf tests", default=".")
|
|
104
|
+
parser.add_argument("--context", "-c", help="tmt style key=value context", action="append")
|
|
37
105
|
cmds = parser.add_subparsers(
|
|
38
|
-
dest=
|
|
106
|
+
dest="_cmd", help="minitmt feature", metavar="<cmd>", required=True,
|
|
39
107
|
)
|
|
40
108
|
|
|
41
109
|
cmd = cmds.add_parser(
|
|
42
|
-
|
|
110
|
+
"discover", aliases=("di",),
|
|
43
111
|
help="list tests, post-processed by tmt plans",
|
|
44
112
|
)
|
|
45
|
-
cmd.add_argument(
|
|
113
|
+
cmd.add_argument("plan", help="tmt plan to use for discovery")
|
|
46
114
|
|
|
47
115
|
cmd = cmds.add_parser(
|
|
48
|
-
|
|
116
|
+
"show",
|
|
49
117
|
help="show fmf data of a test",
|
|
50
118
|
)
|
|
51
|
-
cmd.add_argument(
|
|
52
|
-
cmd.add_argument(
|
|
119
|
+
cmd.add_argument("plan", help="tmt plan to use for discovery")
|
|
120
|
+
cmd.add_argument("test", help="fmf style test regex")
|
|
53
121
|
|
|
54
122
|
cmd = cmds.add_parser(
|
|
55
|
-
|
|
123
|
+
"execute", aliases=("ex",),
|
|
56
124
|
help="run a plan (or test) on a remote system",
|
|
57
125
|
)
|
|
58
|
-
grp = cmd.add_mutually_exclusive_group()
|
|
59
|
-
grp.add_argument(
|
|
60
|
-
grp.add_argument(
|
|
61
|
-
cmd.add_argument(
|
|
62
|
-
cmd.add_argument(
|
|
126
|
+
#grp = cmd.add_mutually_exclusive_group()
|
|
127
|
+
#grp.add_argument("--test", "-t", help="fmf style test regex")
|
|
128
|
+
#grp.add_argument("--plan", "-p", help="tmt plan name (path) inside metadata root")
|
|
129
|
+
cmd.add_argument("--env", "-e", help="environment to pass to prepare/test", action="append")
|
|
130
|
+
cmd.add_argument("--test", "-t", help="fmf style test regex")
|
|
131
|
+
cmd.add_argument(
|
|
132
|
+
"--plan", "-p", help="tmt plan name (path) inside metadata root", required=True,
|
|
133
|
+
)
|
|
134
|
+
cmd.add_argument("--platform", help="platform name, ie. rhel9@x86_64", required=True)
|
|
135
|
+
cmd.add_argument("--user", help="ssh user to connect via", required=True)
|
|
136
|
+
cmd.add_argument("--host", help="ssh host to connect to", required=True)
|
|
137
|
+
cmd.add_argument(
|
|
138
|
+
"--ssh-identity", help="path to a ssh keyfile for login", required=True,
|
|
139
|
+
)
|
|
140
|
+
cmd.add_argument(
|
|
141
|
+
"--results-csv", help="path to would-be-created .csv.gz results", required=True,
|
|
142
|
+
)
|
|
143
|
+
cmd.add_argument(
|
|
144
|
+
"--results-dir", help="path to would-be-created dir for uploaded files", required=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
cmd = cmds.add_parser(
|
|
148
|
+
"setup-script",
|
|
149
|
+
help="generate a script prepping tests for run",
|
|
150
|
+
)
|
|
151
|
+
cmd.add_argument("--remote-root", help="path to tests repo on the remote", required=True)
|
|
152
|
+
cmd.add_argument("--script-debug", help="do 'set -x' in the script", action="store_true")
|
|
153
|
+
cmd.add_argument("plan", help="tmt plan to use for discovery")
|
|
154
|
+
cmd.add_argument("test", help="full fmf test name (not regex)")
|
|
63
155
|
|
|
64
156
|
|
|
65
157
|
def main(args):
|
|
66
|
-
if args._cmd in (
|
|
158
|
+
if args._cmd in ("discover", "di"):
|
|
67
159
|
discover(args)
|
|
68
|
-
elif args._cmd ==
|
|
160
|
+
elif args._cmd == "show":
|
|
69
161
|
show(args)
|
|
70
|
-
elif args._cmd in (
|
|
71
|
-
|
|
72
|
-
|
|
162
|
+
elif args._cmd in ("execute", "ex"):
|
|
163
|
+
execute(args)
|
|
164
|
+
elif args._cmd == "setup-script":
|
|
165
|
+
setup_script(args)
|
|
73
166
|
else:
|
|
74
167
|
raise RuntimeError(f"unknown args: {args}")
|
|
75
168
|
|
|
76
169
|
|
|
77
170
|
CLI_SPEC = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
171
|
+
"aliases": ("tmt",),
|
|
172
|
+
"help": "simple test executor using atex.minitmt",
|
|
173
|
+
"args": parse_args,
|
|
174
|
+
"main": main,
|
|
82
175
|
}
|
atex/cli/testingfarm.py
CHANGED
|
@@ -2,15 +2,15 @@ import sys
|
|
|
2
2
|
import pprint
|
|
3
3
|
|
|
4
4
|
from .. import util
|
|
5
|
-
from .. import
|
|
5
|
+
from ..provision.testingfarm import api as tf
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def _get_api(args):
|
|
9
9
|
api_args = {}
|
|
10
10
|
if args.url:
|
|
11
|
-
api_args[
|
|
11
|
+
api_args["url"] = args.url
|
|
12
12
|
if args.token:
|
|
13
|
-
api_args[
|
|
13
|
+
api_args["token"] = args.token
|
|
14
14
|
return tf.TestingFarmAPI(**api_args)
|
|
15
15
|
|
|
16
16
|
|
|
@@ -27,9 +27,9 @@ def whoami(args):
|
|
|
27
27
|
def composes(args):
|
|
28
28
|
api = _get_api(args)
|
|
29
29
|
comps = api.composes(ranch=args.ranch)
|
|
30
|
-
comps_list = comps[
|
|
30
|
+
comps_list = comps["composes"]
|
|
31
31
|
for comp in comps_list:
|
|
32
|
-
print(comp[
|
|
32
|
+
print(comp["name"])
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def get_request(args):
|
|
@@ -56,27 +56,27 @@ def search_requests(args):
|
|
|
56
56
|
if not reply:
|
|
57
57
|
return
|
|
58
58
|
|
|
59
|
-
for req in sorted(reply, key=lambda x: x[
|
|
60
|
-
req_id = req[
|
|
61
|
-
created = req[
|
|
59
|
+
for req in sorted(reply, key=lambda x: x["created"]):
|
|
60
|
+
req_id = req["id"]
|
|
61
|
+
created = req["created"].partition(".")[0]
|
|
62
62
|
|
|
63
63
|
envs = []
|
|
64
|
-
for env in req[
|
|
65
|
-
if
|
|
66
|
-
compose = env[
|
|
67
|
-
arch = env[
|
|
64
|
+
for env in req["environments_requested"]:
|
|
65
|
+
if "os" in env and env["os"] and "compose" in env["os"]:
|
|
66
|
+
compose = env["os"]["compose"]
|
|
67
|
+
arch = env["arch"]
|
|
68
68
|
if compose and arch:
|
|
69
|
-
envs.append(f
|
|
70
|
-
envs_str =
|
|
69
|
+
envs.append(f"{compose}@{arch}")
|
|
70
|
+
envs_str = ", ".join(envs)
|
|
71
71
|
|
|
72
|
-
print(f
|
|
72
|
+
print(f"{created} {req_id} : {envs_str}")
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def reserve(args):
|
|
76
76
|
util.info(f"Reserving {args.compose} on {args.arch} for {args.timeout} minutes")
|
|
77
77
|
|
|
78
78
|
if args.hvm:
|
|
79
|
-
hardware = {
|
|
79
|
+
hardware = {"virtualization": {"is-supported": True}}
|
|
80
80
|
else:
|
|
81
81
|
hardware = None
|
|
82
82
|
|
|
@@ -96,12 +96,12 @@ def reserve(args):
|
|
|
96
96
|
res.request.assert_alive()
|
|
97
97
|
except tf.GoneAwayError as e:
|
|
98
98
|
print(e)
|
|
99
|
-
raise SystemExit(1)
|
|
99
|
+
raise SystemExit(1) from None
|
|
100
100
|
|
|
101
101
|
proc = util.subprocess_run([
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
f
|
|
102
|
+
"ssh", "-q", "-i", m.ssh_key,
|
|
103
|
+
"-oStrictHostKeyChecking=no", "-oUserKnownHostsFile=/dev/null",
|
|
104
|
+
f"{m.user}@{m.host}",
|
|
105
105
|
])
|
|
106
106
|
if proc.returncode != 0:
|
|
107
107
|
print(
|
|
@@ -123,7 +123,7 @@ def watch_pipeline(args):
|
|
|
123
123
|
|
|
124
124
|
util.info(f"Waiting for {args.request_id} to be 'running'")
|
|
125
125
|
try:
|
|
126
|
-
request.wait_for_state(
|
|
126
|
+
request.wait_for_state("running")
|
|
127
127
|
except tf.GoneAwayError:
|
|
128
128
|
util.info(f"Request {args.request_id} already finished")
|
|
129
129
|
return
|
|
@@ -132,96 +132,96 @@ def watch_pipeline(args):
|
|
|
132
132
|
try:
|
|
133
133
|
for line in tf.PipelineLogStreamer(request):
|
|
134
134
|
sys.stdout.write(line)
|
|
135
|
-
sys.stdout.write(
|
|
135
|
+
sys.stdout.write("\n")
|
|
136
136
|
except tf.GoneAwayError:
|
|
137
137
|
util.info(f"Request {args.request_id} finished, exiting")
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def parse_args(parser):
|
|
141
|
-
parser.add_argument(
|
|
142
|
-
parser.add_argument(
|
|
141
|
+
parser.add_argument("--url", help="Testing Farm API URL")
|
|
142
|
+
parser.add_argument("--token", help="Testing Farm API auth token")
|
|
143
143
|
cmds = parser.add_subparsers(
|
|
144
|
-
dest=
|
|
144
|
+
dest="_cmd", help="TF helper to run", metavar="<cmd>", required=True,
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
cmd = cmds.add_parser(
|
|
148
|
-
|
|
148
|
+
"whoami",
|
|
149
149
|
help="print out details about active TF token",
|
|
150
150
|
)
|
|
151
151
|
cmd = cmds.add_parser(
|
|
152
|
-
|
|
152
|
+
"about",
|
|
153
153
|
help="print out details about TF instance (url)",
|
|
154
154
|
)
|
|
155
155
|
|
|
156
156
|
cmd = cmds.add_parser(
|
|
157
|
-
|
|
157
|
+
"composes",
|
|
158
158
|
help="list all composes available on a given ranch",
|
|
159
159
|
)
|
|
160
|
-
cmd.add_argument(
|
|
160
|
+
cmd.add_argument("ranch", nargs="?", help="Testing Farm ranch (autodetected if token)")
|
|
161
161
|
|
|
162
162
|
cmd = cmds.add_parser(
|
|
163
|
-
|
|
163
|
+
"get-request", aliases=("gr",),
|
|
164
164
|
help="retrieve and print JSON of a Testing Farm request",
|
|
165
165
|
)
|
|
166
|
-
cmd.add_argument(
|
|
166
|
+
cmd.add_argument("request_id", help="Testing Farm request UUID")
|
|
167
167
|
|
|
168
168
|
cmd = cmds.add_parser(
|
|
169
|
-
|
|
169
|
+
"cancel",
|
|
170
170
|
help="cancel a Testing Farm request",
|
|
171
171
|
)
|
|
172
|
-
cmd.add_argument(
|
|
172
|
+
cmd.add_argument("request_id", help="Testing Farm request UUID")
|
|
173
173
|
|
|
174
174
|
cmd = cmds.add_parser(
|
|
175
|
-
|
|
175
|
+
"search-requests", aliases=("sr",),
|
|
176
176
|
help="return a list of requests matching the criteria",
|
|
177
177
|
)
|
|
178
|
-
cmd.add_argument(
|
|
179
|
-
cmd.add_argument(
|
|
180
|
-
cmd.add_argument(
|
|
181
|
-
cmd.add_argument(
|
|
182
|
-
cmd.add_argument(
|
|
178
|
+
cmd.add_argument("--state", help="request state (running, etc.)", required=True)
|
|
179
|
+
cmd.add_argument("--all", help="all requests, not just owned by token", action="store_true")
|
|
180
|
+
cmd.add_argument("--ranch", help="Testing Farm ranch")
|
|
181
|
+
cmd.add_argument("--before", help="only requests created before ISO8601")
|
|
182
|
+
cmd.add_argument("--after", help="only requests created after ISO8601")
|
|
183
183
|
|
|
184
184
|
cmd = cmds.add_parser(
|
|
185
|
-
|
|
185
|
+
"reserve",
|
|
186
186
|
help="reserve a system and ssh into it",
|
|
187
187
|
)
|
|
188
|
-
cmd.add_argument(
|
|
189
|
-
cmd.add_argument(
|
|
190
|
-
cmd.add_argument(
|
|
191
|
-
cmd.add_argument(
|
|
192
|
-
cmd.add_argument(
|
|
188
|
+
cmd.add_argument("--compose", "-c", help="OS compose to install", required=True)
|
|
189
|
+
cmd.add_argument("--arch", "-a", help="system HW architecture", default="x86_64")
|
|
190
|
+
cmd.add_argument("--timeout", "-t", help="pipeline timeout (in minutes)", type=int, default=60)
|
|
191
|
+
cmd.add_argument("--ssh-key", help="path to a ssh private key file like 'id_rsa'")
|
|
192
|
+
cmd.add_argument("--hvm", help="request a HVM virtualization capable HW", action="store_true")
|
|
193
193
|
|
|
194
194
|
cmd = cmds.add_parser(
|
|
195
|
-
|
|
195
|
+
"watch-pipeline", aliases=("wp",),
|
|
196
196
|
help="continuously output pipeline.log like 'tail -f'",
|
|
197
197
|
)
|
|
198
|
-
cmd.add_argument(
|
|
198
|
+
cmd.add_argument("request_id", help="Testing Farm request UUID")
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def main(args):
|
|
202
|
-
if args._cmd ==
|
|
202
|
+
if args._cmd == "whoami":
|
|
203
203
|
whoami(args)
|
|
204
|
-
elif args._cmd ==
|
|
204
|
+
elif args._cmd == "about":
|
|
205
205
|
about(args)
|
|
206
|
-
elif args._cmd ==
|
|
206
|
+
elif args._cmd == "composes":
|
|
207
207
|
composes(args)
|
|
208
|
-
elif args._cmd in (
|
|
208
|
+
elif args._cmd in ("get-request", "gr"):
|
|
209
209
|
get_request(args)
|
|
210
|
-
elif args._cmd ==
|
|
210
|
+
elif args._cmd == "cancel":
|
|
211
211
|
cancel(args)
|
|
212
|
-
elif args._cmd in (
|
|
212
|
+
elif args._cmd in ("search-requests", "sr"):
|
|
213
213
|
search_requests(args)
|
|
214
|
-
elif args._cmd ==
|
|
214
|
+
elif args._cmd == "reserve":
|
|
215
215
|
reserve(args)
|
|
216
|
-
elif args._cmd in (
|
|
216
|
+
elif args._cmd in ("watch-pipeline", "wp"):
|
|
217
217
|
watch_pipeline(args)
|
|
218
218
|
else:
|
|
219
219
|
raise RuntimeError(f"unknown args: {args}")
|
|
220
220
|
|
|
221
221
|
|
|
222
222
|
CLI_SPEC = {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
"aliases": ("tf",),
|
|
224
|
+
"help": "various utils for Testing Farm",
|
|
225
|
+
"args": parse_args,
|
|
226
|
+
"main": main,
|
|
227
227
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import importlib as _importlib
|
|
2
|
+
import pkgutil as _pkgutil
|
|
3
|
+
import threading as _threading
|
|
4
|
+
|
|
5
|
+
from .. import util as _util
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Connection:
|
|
9
|
+
"""
|
|
10
|
+
A unified API for connecting to a remote system, running multiple commands,
|
|
11
|
+
rsyncing files to/from it and checking for connection state.
|
|
12
|
+
|
|
13
|
+
conn = Connection()
|
|
14
|
+
conn.connect()
|
|
15
|
+
proc = conn.cmd(["ls", "/"])
|
|
16
|
+
#proc = conn.cmd(["ls", "/"], func=subprocess.Popen) # non-blocking
|
|
17
|
+
#output = conn.cmd(["ls", "/"], func=subprocess.check_output) # stdout
|
|
18
|
+
conn.rsync("-v", "remote:/etc/passwd", "passwd")
|
|
19
|
+
conn.disconnect()
|
|
20
|
+
|
|
21
|
+
# or as try/except/finally
|
|
22
|
+
conn = Connection()
|
|
23
|
+
try:
|
|
24
|
+
conn.connect()
|
|
25
|
+
...
|
|
26
|
+
finally:
|
|
27
|
+
conn.disconnect()
|
|
28
|
+
|
|
29
|
+
# or via Context Manager
|
|
30
|
+
with Connection() as conn:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
Note that internal connection handling must be implemented as thread-aware,
|
|
34
|
+
ie. disconnect() might be called from a different thread while connect()
|
|
35
|
+
or cmd() are still running.
|
|
36
|
+
Similarly, multiple threads may run cmd() or rsync() independently.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the connection instance.
|
|
42
|
+
If extending __init__, always call 'super().__init__()' at the top.
|
|
43
|
+
"""
|
|
44
|
+
self.lock = _threading.RLock()
|
|
45
|
+
|
|
46
|
+
def __enter__(self):
|
|
47
|
+
self.connect()
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
51
|
+
self.disconnect()
|
|
52
|
+
|
|
53
|
+
def connect(self, block=True):
|
|
54
|
+
"""
|
|
55
|
+
Establish a persistent connection to the remote.
|
|
56
|
+
|
|
57
|
+
If 'block' is True, wait for the connection to be up,
|
|
58
|
+
otherwise raise BlockingIOError if the connection is still down.
|
|
59
|
+
"""
|
|
60
|
+
raise NotImplementedError(f"'connect' not implemented for {self.__class__.__name__}")
|
|
61
|
+
|
|
62
|
+
def disconnect(self):
|
|
63
|
+
"""
|
|
64
|
+
Destroy the persistent connection to the remote.
|
|
65
|
+
"""
|
|
66
|
+
raise NotImplementedError(f"'disconnect' not implemented for {self.__class__.__name__}")
|
|
67
|
+
|
|
68
|
+
# TODO: is this needed? .. we probably want Remote.alive() instead
|
|
69
|
+
#def alive(self):
|
|
70
|
+
# """
|
|
71
|
+
# Return True if the connection was established and is active,
|
|
72
|
+
# False otherwise.
|
|
73
|
+
# """
|
|
74
|
+
# raise NotImplementedError(f"'alive' not implemented for {self.__class__.__name__}")
|
|
75
|
+
|
|
76
|
+
def cmd(self, command, func=_util.subprocess_run, **func_args):
|
|
77
|
+
"""
|
|
78
|
+
Execute a single command on the remote, using subprocess-like semantics.
|
|
79
|
+
|
|
80
|
+
'command' is the command with arguments, as a tuple/list.
|
|
81
|
+
|
|
82
|
+
'func' is the subprocess function to use (.run(), .Popen, etc.).
|
|
83
|
+
|
|
84
|
+
'func_args' are further keyword arguments to pass to 'func'.
|
|
85
|
+
"""
|
|
86
|
+
raise NotImplementedError(f"'cmd' not implemented for {self.__class__.__name__}")
|
|
87
|
+
|
|
88
|
+
def rsync(self, *args, func=_util.subprocess_run, **func_args):
|
|
89
|
+
"""
|
|
90
|
+
Synchronize local/remote files/directories via 'rsync'.
|
|
91
|
+
|
|
92
|
+
Pass *args like rsync(1) CLI arguments, incl. option arguments, ie.
|
|
93
|
+
.rsync("-vr", "local_path/", "remote:remote_path")
|
|
94
|
+
.rsync("-z", "remote:remote_file" ".")
|
|
95
|
+
|
|
96
|
+
To indicate remote path, use any string followed by a colon, the remote
|
|
97
|
+
name does not matter as an internally-handled '-e' option dictates all
|
|
98
|
+
the connection details.
|
|
99
|
+
|
|
100
|
+
'func' is a subprocess function to use (.run(), .Popen, etc.).
|
|
101
|
+
|
|
102
|
+
'func_args' are further keyword arguments to pass to 'func'.
|
|
103
|
+
|
|
104
|
+
The remote must have rsync(1) already installed.
|
|
105
|
+
"""
|
|
106
|
+
raise NotImplementedError(f"'rsync' not implemented for {self.__class__.__name__}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_submodules = [
|
|
110
|
+
info.name for info in _pkgutil.iter_modules(__spec__.submodule_search_locations)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
__all__ = [*_submodules, Connection.__name__] # noqa: PLE0604
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def __dir__():
|
|
117
|
+
return __all__
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# lazily import submodules
|
|
121
|
+
def __getattr__(attr):
|
|
122
|
+
if attr in _submodules:
|
|
123
|
+
return _importlib.import_module(f".{attr}", __name__)
|
|
124
|
+
else:
|
|
125
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{attr}'")
|