atex 0.5__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.
Files changed (46) hide show
  1. atex/__init__.py +2 -12
  2. atex/cli/__init__.py +13 -13
  3. atex/cli/fmf.py +93 -0
  4. atex/cli/testingfarm.py +71 -61
  5. atex/connection/__init__.py +117 -0
  6. atex/connection/ssh.py +390 -0
  7. atex/executor/__init__.py +2 -0
  8. atex/executor/duration.py +60 -0
  9. atex/executor/executor.py +378 -0
  10. atex/executor/reporter.py +106 -0
  11. atex/executor/scripts.py +155 -0
  12. atex/executor/testcontrol.py +353 -0
  13. atex/fmf.py +217 -0
  14. atex/orchestrator/__init__.py +2 -0
  15. atex/orchestrator/aggregator.py +106 -0
  16. atex/orchestrator/orchestrator.py +324 -0
  17. atex/provision/__init__.py +101 -90
  18. atex/provision/libvirt/VM_PROVISION +8 -0
  19. atex/provision/libvirt/__init__.py +4 -4
  20. atex/provision/podman/README +59 -0
  21. atex/provision/podman/host_container.sh +74 -0
  22. atex/provision/testingfarm/__init__.py +2 -0
  23. atex/{testingfarm.py → provision/testingfarm/api.py} +170 -132
  24. atex/provision/testingfarm/testingfarm.py +236 -0
  25. atex/util/__init__.py +5 -10
  26. atex/util/dedent.py +1 -1
  27. atex/util/log.py +20 -12
  28. atex/util/path.py +16 -0
  29. atex/util/ssh_keygen.py +14 -0
  30. atex/util/subprocess.py +14 -13
  31. atex/util/threads.py +55 -0
  32. {atex-0.5.dist-info → atex-0.8.dist-info}/METADATA +97 -2
  33. atex-0.8.dist-info/RECORD +37 -0
  34. atex/cli/minitmt.py +0 -82
  35. atex/minitmt/__init__.py +0 -115
  36. atex/minitmt/fmf.py +0 -168
  37. atex/minitmt/report.py +0 -174
  38. atex/minitmt/scripts.py +0 -51
  39. atex/minitmt/testme.py +0 -3
  40. atex/orchestrator.py +0 -38
  41. atex/ssh.py +0 -320
  42. atex/util/lockable_class.py +0 -38
  43. atex-0.5.dist-info/RECORD +0 -26
  44. {atex-0.5.dist-info → atex-0.8.dist-info}/WHEEL +0 -0
  45. {atex-0.5.dist-info → atex-0.8.dist-info}/entry_points.txt +0 -0
  46. {atex-0.5.dist-info → atex-0.8.dist-info}/licenses/COPYING.txt +0 -0
atex/ssh.py DELETED
@@ -1,320 +0,0 @@
1
- import os
2
- import time
3
- import tempfile
4
- import subprocess
5
- from pathlib import Path
6
-
7
- from . import util
8
-
9
- DEFAULT_OPTIONS = {
10
- 'LogLevel': 'ERROR',
11
- 'StrictHostKeyChecking': 'no',
12
- 'UserKnownHostsFile': '/dev/null',
13
- 'ConnectionAttempts': '3',
14
- 'ServerAliveCountMax': '4',
15
- 'ServerAliveInterval': '5',
16
- 'TCPKeepAlive': 'no',
17
- 'EscapeChar': 'none',
18
- 'ExitOnForwardFailure': 'yes',
19
- # 'RequestTTY': 'force',
20
- }
21
-
22
-
23
- def _shell_cmd(args, sudo=None):
24
- """
25
- Make a command line for running 'args' on the target system.
26
- """
27
- if sudo:
28
- cmd = ('exec', 'sudo', '--no-update', '--non-interactive', '--user', sudo, '--', *args)
29
- else:
30
- cmd = ('exec', '--', *args)
31
- return ' '.join(cmd)
32
-
33
-
34
- def _options_to_cli(options):
35
- """
36
- Assemble an ssh(1) or sshpass(1) command line with -o options.
37
- """
38
- list_opts = []
39
- for key, value in options.items():
40
- if isinstance(value, str):
41
- list_opts.append(f'-o{key}={value}')
42
- else:
43
- if len(value) == 0:
44
- raise ValueError(f"key {key} has an empty iterable value")
45
- list_opts += (f'-o{key}={v}' for v in value)
46
- return list_opts
47
-
48
-
49
- def _options_to_ssh(options, password=None, extra_cli_flags=()):
50
- """
51
- Assemble an ssh(1) or sshpass(1) command line with -o options.
52
- """
53
- cli_opts = _options_to_cli(options)
54
- if password:
55
- return (
56
- 'sshpass', '-p', password,
57
- 'ssh', *extra_cli_flags, '-oBatchMode=no', *cli_opts,
58
- 'ignored_arg',
59
- )
60
- else:
61
- # let cli_opts override BatchMode if specified
62
- return ('ssh', *extra_cli_flags, *cli_opts, '-oBatchMode=yes', 'ignored_arg')
63
-
64
-
65
- # return a string usable for rsync -e
66
- def _options_to_rsync_e(options, password=None):
67
- """
68
- Return a string usable for the rsync -e argument.
69
- """
70
- cli_opts = _options_to_cli(options)
71
- batch_mode = '-oBatchMode=no' if password else '-oBatchMode=yes'
72
- return ' '.join(('ssh', *cli_opts, batch_mode)) # no ignored_arg inside -e
73
-
74
-
75
- def _rsync_host_cmd(*args, options, password=None, sudo=None):
76
- """
77
- Assemble a rsync command line, noting that
78
- - 'sshpass' must be before 'rsync', not inside the '-e' argument
79
- - 'ignored_arg' must be passed by user as destination, not inside '-e'
80
- - 'sudo' is part of '--rsync-path', yet another argument
81
- """
82
- return (
83
- *(('sshpass', '-p', password) if password else ()),
84
- 'rsync',
85
- '-e', _options_to_rsync_e(options, password=password),
86
- '--rsync-path', _shell_cmd(('rsync',), sudo=sudo),
87
- *args,
88
- )
89
-
90
-
91
- class SSHConn:
92
- r"""
93
- Represents a persistent SSH connection to a host (ControlMaster).
94
-
95
- When instantiated, it attempts to connect to the specified host, with any
96
- subsequent instance method calls using that connection, or raising
97
- ConnectionResetError when it is lost.
98
-
99
- The ssh(1) command is parametrized purely and solely via ssh_config(5)
100
- options, including 'Hostname', 'User', 'Port', etc.
101
- Pass any overrides or missing options as 'options' (dict).
102
-
103
- options = {
104
- 'Hostname': '1.2.3.4',
105
- 'User': 'testuser',
106
- 'IdentityFile': '/home/testuser/.ssh/id_rsa',
107
- }
108
-
109
- # with a persistent ControlMaster
110
- conn = SSHConn(options)
111
- conn.connect()
112
- output = conn.run('ls /')
113
- #proc = conn.Popen('ls /') # non-blocking
114
- conn.disconnect()
115
-
116
- # or as try/except/finally
117
- conn = SSHConn(options)
118
- try:
119
- conn.connect()
120
- ...
121
- finally:
122
- conn.disconnect()
123
-
124
- # or via Context Manager
125
- with SSHConn(options) as conn:
126
- ...
127
- """
128
-
129
- def __init__(self, options, *, password=None):
130
- """
131
- Connect to an SSH server specified in 'options'.
132
-
133
- If 'password' is given, spawn the ssh(1) command via 'sshpass' and
134
- pass the password to it.
135
-
136
- If 'sudo' specifies a username, call sudo(8) on the remote shell
137
- to run under a different user on the remote host.
138
- """
139
- self.options = DEFAULT_OPTIONS.copy()
140
- self.options.update(options)
141
- self.password = password
142
- self.tmpdir = None
143
- self.proc_master = None
144
-
145
- def _assert_master(self):
146
- proc = self.master_proc
147
- if not proc:
148
- raise RuntimeError("SSH ControlMaster is not running")
149
- # we need to consume any potential proc output for the process to
150
- # actually terminate (stop being a zombie) if it crashes
151
- proc = self.master_proc
152
- out = proc.stdout.read()
153
- code = proc.poll()
154
- if code is not None:
155
- self.master_proc = None
156
- raise RuntimeError(f"SSH ControlMaster on {self.tmpdir} exited with {code}:\n{out}")
157
-
158
- def disconnect(self):
159
- proc = self.master_proc
160
- if not proc:
161
- return
162
- proc.terminate()
163
- # don't zombie forever, return EPIPE on any attempts to write to us
164
- proc.stdout.close()
165
- proc.wait()
166
- self.master_proc = None
167
-
168
- def connect(self):
169
- if self.proc_master:
170
- raise FileExistsError(f"SSH ControlMaster process already running on {self.tmpdir}")
171
-
172
- if not self.tmpdir:
173
- self.tmpdir_handle = tempfile.TemporaryDirectory(prefix='atex-ssh-')
174
- self.tmpdir = Path(self.tmpdir_handle.name)
175
-
176
- options = self.options.copy()
177
- options['SessionType'] = 'none'
178
- options['ControlMaster'] = 'yes'
179
- sock = self.tmpdir / 'control.sock'
180
- options['ControlPath'] = sock
181
-
182
- # TODO: shouldn't password be handled here and passed to _options_to_ssh
183
- # rather than for member .ssh() ?
184
-
185
- proc = util.subprocess_Popen(
186
- _options_to_ssh(options),
187
- stdin=subprocess.DEVNULL,
188
- stdout=subprocess.PIPE,
189
- stderr=subprocess.STDOUT,
190
- cwd=str(self.tmpdir),
191
- )
192
- os.set_blocking(proc.stdout.fileno(), False)
193
-
194
- # wait for the master to either create the socket (indicating valid
195
- # connection) or give up and exit
196
- while proc.poll() is None:
197
- if sock.exists():
198
- break
199
- time.sleep(0.1)
200
- else:
201
- code = proc.poll()
202
- out = proc.stdout.read()
203
- raise FileNotFoundError(
204
- f"SSH ControlMaster failed to start on {self.tmpdir} with {code}:\n{out}",
205
- )
206
-
207
- self.master_proc = proc
208
-
209
- def add_local_forward(self, *spec):
210
- """
211
- Add (one or more) ssh forwarding specifications as 'spec' to an
212
- already-connected instance. Each specification has to follow the
213
- format of ssh client's LocalForward option (see ssh_config(5)).
214
- """
215
- self._assert_master()
216
- options = self.options.copy()
217
- options['LocalForward'] = spec
218
- options['ControlPath'] = self.tmpdir / 'control.sock'
219
- util.subprocess_run(
220
- _options_to_ssh(options, extra_cli_flags=('-O', 'forward')),
221
- skip_frames=1,
222
- check=True,
223
- )
224
-
225
- def add_remote_forward(self, *spec):
226
- """
227
- Add (one or more) ssh forwarding specifications as 'spec' to an
228
- already-connected instance. Each specification has to follow the
229
- format of ssh client's RemoteForward option (see ssh_config(5)).
230
- """
231
- self._assert_master()
232
- options = self.options.copy()
233
- options['RemoteForward'] = spec
234
- options['ControlPath'] = self.tmpdir / 'control.sock'
235
- util.subprocess_run(
236
- _options_to_ssh(options, extra_cli_flags=('-O', 'forward')),
237
- skip_frames=1,
238
- check=True,
239
- )
240
-
241
- def __enter__(self):
242
- self.connect()
243
- return self
244
-
245
- def __exit__(self, exc_type, exc_value, traceback):
246
- self.disconnect()
247
-
248
- def ssh(
249
- self, cmd, *args, options=None, sudo=None, text=True,
250
- func=util.subprocess_run, **run_kwargs,
251
- ):
252
- self._assert_master()
253
- unified_options = self.options.copy()
254
- if options:
255
- unified_options.update(options)
256
- unified_options['RemoteCommand'] = _shell_cmd((cmd, *args), sudo=sudo)
257
- unified_options['ControlPath'] = self.tmpdir / 'control.sock'
258
- return func(
259
- _options_to_ssh(unified_options, password=self.password),
260
- skip_frames=1,
261
- text=text,
262
- **run_kwargs,
263
- )
264
-
265
- def rsync(
266
- self, *args, options=None, sudo=None,
267
- func=util.subprocess_run, **run_kwargs,
268
- ):
269
- self._assert_master()
270
- unified_options = self.options.copy()
271
- if options:
272
- unified_options.update(options)
273
- unified_options['ControlPath'] = self.tmpdir / 'control.sock'
274
- return func(
275
- _rsync_host_cmd(*args, options=unified_options, password=self.password, sudo=sudo),
276
- skip_frames=1,
277
- text=True,
278
- stdin=subprocess.DEVNULL,
279
- **run_kwargs,
280
- )
281
-
282
-
283
- # have options as kwarg to be compatible with other functions here
284
- def ssh(
285
- cmd, *args, options, password=None, sudo=None, text=True,
286
- func=util.subprocess_run, **run_kwargs,
287
- ):
288
- """
289
- Execute ssh(1) with the given options.
290
-
291
- On the remote system, run 'cmd' in a shell.
292
-
293
- If 'password' is given, spawn the ssh(1) command via 'sshpass' and
294
- pass the password to it.
295
-
296
- If 'sudo' specifies a username, call sudo(8) on the remote shell
297
- to run under a different user on the remote host.
298
- """
299
- unified_options = DEFAULT_OPTIONS.copy()
300
- unified_options['RemoteCommand'] = _shell_cmd((cmd, *args), sudo=sudo)
301
- unified_options.update(options)
302
- return func(
303
- _options_to_ssh(unified_options, password=password),
304
- skip_frames=1,
305
- text=text,
306
- **run_kwargs,
307
- )
308
-
309
-
310
- def rsync(
311
- *args, options, password=None, sudo=None,
312
- func=util.subprocess_run, **run_kwargs,
313
- ):
314
- return func(
315
- _rsync_host_cmd(*args, options=options, password=password, sudo=sudo),
316
- skip_frames=1,
317
- text=True,
318
- stdin=subprocess.DEVNULL,
319
- **run_kwargs,
320
- )
@@ -1,38 +0,0 @@
1
- import threading
2
-
3
-
4
- class LockableClass:
5
- """
6
- A class with (nearly) all attribute accesses protected by threading.RLock,
7
- making them thread-safe at the cost of some speed.
8
-
9
- class MyClass(LockableClass):
10
- def writer(self):
11
- self.attr = 222 # thread-safe instance access
12
- def reader(self):
13
- print(self.attr) # thread-safe instance access
14
- def complex(self):
15
- with self.lock: # thread-safe context
16
- self.attr += 1
17
-
18
- Here, 'lock' is a reserved attribute name and must not be overriden
19
- by a derived class.
20
-
21
- If overriding '__init__', make sure to call 'super().__init__()' *before*
22
- any attribute accesses in your '__init__'.
23
- """
24
- def __init__(self):
25
- object.__setattr__(self, 'lock', threading.RLock())
26
-
27
- def __getattribute__(self, name):
28
- # optimize built-ins
29
- if name.startswith('__') or name == 'lock':
30
- return object.__getattribute__(self, name)
31
- lock = object.__getattribute__(self, 'lock')
32
- with lock:
33
- return object.__getattribute__(self, name)
34
-
35
- def __setattr__(self, name, value):
36
- lock = object.__getattribute__(self, 'lock')
37
- with lock:
38
- object.__setattr__(self, name, value)
atex-0.5.dist-info/RECORD DELETED
@@ -1,26 +0,0 @@
1
- atex/__init__.py,sha256=YyU5HzPiApngH_ZS7d5MhqpPvNgFXi7FpXWnkdhtQ1A,859
2
- atex/orchestrator.py,sha256=SqP07lsjBImdGN4E5jd816nvcHQxd0b0SMnJDy7KkLA,1083
3
- atex/ssh.py,sha256=CXLkqrZ1lByI7CRGAZAYS0ye0pUj8_5x2OOnYH4iyAU,10188
4
- atex/testingfarm.py,sha256=4ZXB9YPMFX_pO1yNVIbZX5elUTFSwFJnYHr4v35WBTU,18557
5
- atex/cli/__init__.py,sha256=hahfD6XqsS4GwvJ1Ou_WpEtR4xcC5IUq-rG11xGhLDU,2406
6
- atex/cli/minitmt.py,sha256=aLJhqwAA4BKgf2nTJUX-iK0ECQSNBSvYSTSlDqPu7qQ,2403
7
- atex/cli/testingfarm.py,sha256=xMV-J3IodqDFFiQehiTn_Skn6vcYkuy0krLHdByZ7C8,6683
8
- atex/minitmt/__init__.py,sha256=MasgXrgBBIyNT0tL-yI2jyKfPII_ektTDbT0Fh9lybI,4210
9
- atex/minitmt/fmf.py,sha256=HEpjCVyPeX7ImebL0u5d2wETYLCnLcz41FtGJXigCrU,6252
10
- atex/minitmt/report.py,sha256=AyZG5yDBhe8ajAjpwJhdms4f-CeFbXO6L9sibN65-wU,6036
11
- atex/minitmt/scripts.py,sha256=41DmyAwId5OAiP9g1VjslNaJ6iz2bCXaqBrHS3uZN7g,1620
12
- atex/minitmt/testme.py,sha256=aVh9Kjjenr8Q52x3nOhBFx6bGnxpM-amoVw-2JGvZVo,39
13
- atex/provision/__init__.py,sha256=2haBx9lPk0FHpYxykzfSwKkFLsaAcBjRCLoJE82hpT8,4029
14
- atex/provision/libvirt/VM_PROVISION,sha256=1OwGC8fEA74RaCC2yyWSnnML-QXlB_cPzhTxjsXofsQ,2469
15
- atex/provision/libvirt/__init__.py,sha256=IjcmbqD6SF_wP1LUISlwqw6mZ2opXEOfckXO_oCtsgc,787
16
- atex/provision/libvirt/setup-libvirt.sh,sha256=CXrEFdrj8CSHXQZCd2RWuRvTmw7QYFTVhZeLuhhXooI,1855
17
- atex/util/__init__.py,sha256=mejFs_IHvKqnuOA5xt60CWzX6fJ4VVx8T1kl1zuv5Vg,1550
18
- atex/util/dedent.py,sha256=uJYXKsEQECbJ58eqZFI5Bzlld0lD-C15NUcqRnvrLa4,603
19
- atex/util/lockable_class.py,sha256=hPuiPaUBUD-9bKLzaYYOrQ6JwLjk2zCzCkW8nm6SKpQ,1317
20
- atex/util/log.py,sha256=tQp4JZY1B4bca5CciscV4Q5ZsyKS2niOQb9_Vco_QOA,1776
21
- atex/util/subprocess.py,sha256=IWrwke4JylL6-jQoNMXnzKG4MVxbc9m6Q4wJVy4EBCM,1700
22
- atex-0.5.dist-info/METADATA,sha256=T1m2AdN1nkUMk3OKkdXEJ94V4xftTiJvMlUovPNzDi0,5076
23
- atex-0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
- atex-0.5.dist-info/entry_points.txt,sha256=pLqJdcfeyQTgup2h6dWb6SvkHhtOl-W5Eg9zV8moK0o,39
25
- atex-0.5.dist-info/licenses/COPYING.txt,sha256=oEuj51jdmbXcCUy7pZ-KE0BNcJTR1okudRp5zQ0yWnU,670
26
- atex-0.5.dist-info/RECORD,,
File without changes