atex 0.5__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.5.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.5.dist-info/RECORD +0 -26
- {atex-0.5.dist-info → atex-0.7.dist-info}/WHEEL +0 -0
- {atex-0.5.dist-info → atex-0.7.dist-info}/entry_points.txt +0 -0
- {atex-0.5.dist-info → atex-0.7.dist-info}/licenses/COPYING.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
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,,
|
atex/minitmt/report.py
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import csv
|
|
3
|
-
import gzip
|
|
4
|
-
import ctypes
|
|
5
|
-
import ctypes.util
|
|
6
|
-
import contextlib
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from .. import util
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
|
13
|
-
|
|
14
|
-
# int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags)
|
|
15
|
-
libc.linkat.argtypes = (
|
|
16
|
-
ctypes.c_int,
|
|
17
|
-
ctypes.c_char_p,
|
|
18
|
-
ctypes.c_int,
|
|
19
|
-
ctypes.c_char_p,
|
|
20
|
-
ctypes.c_int,
|
|
21
|
-
)
|
|
22
|
-
libc.linkat.restype = ctypes.c_int
|
|
23
|
-
|
|
24
|
-
# fcntl.h:#define AT_EMPTY_PATH 0x1000 /* Allow empty relative pathname */
|
|
25
|
-
AT_EMPTY_PATH = 0x1000
|
|
26
|
-
|
|
27
|
-
# fcntl.h:#define AT_FDCWD -100 /* Special value used to indicate
|
|
28
|
-
AT_FDCWD = -100
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def linkat(*args):
|
|
32
|
-
if (ret := libc.linkat(*args)) == -1:
|
|
33
|
-
errno = ctypes.get_errno()
|
|
34
|
-
raise OSError(errno, os.strerror(errno))
|
|
35
|
-
return ret
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class CSVReporter(util.LockableClass):
|
|
39
|
-
"""
|
|
40
|
-
Stores reported results as a GZIP-ed CSV and files (logs) under a related
|
|
41
|
-
directory.
|
|
42
|
-
|
|
43
|
-
with CSVReporter('file/storage/dir', 'results.csv.gz') as reporter:
|
|
44
|
-
sub = reporter.make_subreporter('rhel-9', 'x86_64')
|
|
45
|
-
sub({'name': '/some/test', 'status': 'pass'})
|
|
46
|
-
sub({'name': '/another/test', 'status': 'pass'})
|
|
47
|
-
...
|
|
48
|
-
sub = reporter.make_subreporter('rhel-9', 'ppc64le')
|
|
49
|
-
...
|
|
50
|
-
sock = accept_unix_connection()
|
|
51
|
-
reporter.store_file('/some/test', 'debug.log', sock, 1234)
|
|
52
|
-
"""
|
|
53
|
-
class _ExcelWithUnixNewline(csv.excel):
|
|
54
|
-
lineterminator = '\n'
|
|
55
|
-
|
|
56
|
-
def __init__(self, storage_dir, results_file):
|
|
57
|
-
super().__init__()
|
|
58
|
-
self.storage_dir = Path(storage_dir)
|
|
59
|
-
if self.storage_dir.exists():
|
|
60
|
-
raise FileExistsError(f"{storage_dir} already exists")
|
|
61
|
-
self.results_file = Path(results_file)
|
|
62
|
-
if self.results_file.exists():
|
|
63
|
-
raise FileExistsError(f"{self.results_file} already exists")
|
|
64
|
-
self.storage_dir.mkdir()
|
|
65
|
-
self.csv_writer = None
|
|
66
|
-
self.results_gzip_handle = None
|
|
67
|
-
|
|
68
|
-
def __enter__(self):
|
|
69
|
-
f = gzip.open(self.results_file, 'wt', newline='')
|
|
70
|
-
try:
|
|
71
|
-
self.csv_writer = csv.writer(f, dialect=self._ExcelWithUnixNewline)
|
|
72
|
-
except:
|
|
73
|
-
f.close()
|
|
74
|
-
raise
|
|
75
|
-
self.results_gzip_handle = f
|
|
76
|
-
return self
|
|
77
|
-
|
|
78
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
|
79
|
-
self.results_gzip_handle.close()
|
|
80
|
-
self.results_gzip_handle = None
|
|
81
|
-
self.csv_writer = None
|
|
82
|
-
|
|
83
|
-
def report(self, distro, arch, status, name, note, *files):
|
|
84
|
-
"""
|
|
85
|
-
Persistently write out details of a test result.
|
|
86
|
-
"""
|
|
87
|
-
with self.lock:
|
|
88
|
-
self.csv_writer.writerow((distro, arch, status, name, note, *files))
|
|
89
|
-
|
|
90
|
-
@staticmethod
|
|
91
|
-
def _normalize_path(path):
|
|
92
|
-
# the magic here is to treat any dangerous path as starting at /
|
|
93
|
-
# and resolve any weird constructs relative to /, and then simply
|
|
94
|
-
# strip off the leading / and use it as a relative path
|
|
95
|
-
path = path.lstrip('/')
|
|
96
|
-
path = os.path.normpath(f'/{path}')
|
|
97
|
-
return path[1:]
|
|
98
|
-
|
|
99
|
-
def make_subreporter(self, distro, arch):
|
|
100
|
-
"""
|
|
101
|
-
Return a preconfigured reporter instance, suitable for use
|
|
102
|
-
by an Executor.
|
|
103
|
-
"""
|
|
104
|
-
def reporter(result_line):
|
|
105
|
-
if 'files' in result_line:
|
|
106
|
-
files = (self._normalize_path(x['name']) for x in result_line['files'])
|
|
107
|
-
else:
|
|
108
|
-
files = ()
|
|
109
|
-
self.report(
|
|
110
|
-
distro, arch, result_line['status'], result_line['name'],
|
|
111
|
-
result_line.get('note', ''), *files,
|
|
112
|
-
)
|
|
113
|
-
return reporter
|
|
114
|
-
|
|
115
|
-
def _files_dir(self, result_name):
|
|
116
|
-
dir_path = self.storage_dir / result_name.lstrip('/')
|
|
117
|
-
dir_path.mkdir(parents=True, exist_ok=True)
|
|
118
|
-
return dir_path
|
|
119
|
-
|
|
120
|
-
def _files_file(self, result_name, file_name):
|
|
121
|
-
file_name = self._normalize_path(file_name)
|
|
122
|
-
return self._files_dir(result_name) / file_name
|
|
123
|
-
|
|
124
|
-
@contextlib.contextmanager
|
|
125
|
-
def open_tmpfile(self, open_mode=os.O_WRONLY):
|
|
126
|
-
flags = open_mode | os.O_TMPFILE
|
|
127
|
-
fd = os.open(self.storage_dir, flags, 0o644)
|
|
128
|
-
try:
|
|
129
|
-
yield fd
|
|
130
|
-
finally:
|
|
131
|
-
os.close(fd)
|
|
132
|
-
# def open_tmpfile(self, result_name, open_mode=os.O_WRONLY):
|
|
133
|
-
# """
|
|
134
|
-
# Open an anonymous (name-less) file for writing, in a directory relevant
|
|
135
|
-
# to 'result_name' and yield its file descriptor (int) as context, closing
|
|
136
|
-
# it when the context is exited.
|
|
137
|
-
# """
|
|
138
|
-
# flags = open_mode | os.O_TMPFILE
|
|
139
|
-
# fd = os.open(self._files_dir(result_name), flags, 0o644)
|
|
140
|
-
# try:
|
|
141
|
-
# yield fd
|
|
142
|
-
# finally:
|
|
143
|
-
# os.close(fd)
|
|
144
|
-
|
|
145
|
-
def link_tmpfile_to(self, result_name, file_name, fd):
|
|
146
|
-
"""
|
|
147
|
-
Store a file named 'file_name' in a directory relevant to 'result_name'
|
|
148
|
-
whose 'fd' (a file descriptor) was created by open_tmpfile().
|
|
149
|
-
|
|
150
|
-
This function can be called multiple times with the same 'fd', and
|
|
151
|
-
does not close or otherwise alter the descriptor.
|
|
152
|
-
"""
|
|
153
|
-
final_path = self._files_file(result_name, file_name)
|
|
154
|
-
linkat(fd, b'', AT_FDCWD, bytes(final_path), AT_EMPTY_PATH)
|
|
155
|
-
|
|
156
|
-
def store_file(self, result_name, file_name, in_fd, count):
|
|
157
|
-
"""
|
|
158
|
-
Read 'count' bytes of binary data from an OS file descriptor 'in_fd'
|
|
159
|
-
and store them under 'result_name' as a file (or relative path)
|
|
160
|
-
named 'file_name', creating it.
|
|
161
|
-
"""
|
|
162
|
-
final_path = self._files_file(result_name, file_name)
|
|
163
|
-
# be as efficient as possible, let the kernel handle big data
|
|
164
|
-
out_fd = None
|
|
165
|
-
try:
|
|
166
|
-
out_fd = os.open(final_path, os.O_WRONLY | os.O_CREAT)
|
|
167
|
-
while count > 0:
|
|
168
|
-
written = os.sendfile(out_fd, in_fd, None, count)
|
|
169
|
-
if written == 0:
|
|
170
|
-
raise RuntimeError(f"got unexpected EOF when receiving {final_path}")
|
|
171
|
-
count -= written
|
|
172
|
-
finally:
|
|
173
|
-
if out_fd:
|
|
174
|
-
os.close(out_fd)
|
atex/minitmt/testme.py
DELETED
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
|
-
)
|
atex/util/lockable_class.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|