atex 0.13__py3-none-any.whl → 0.15__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 +1 -1
- atex/aggregator/__init__.py +8 -7
- atex/aggregator/json.py +45 -42
- atex/cli/__init__.py +12 -14
- atex/cli/testingfarm.py +6 -6
- atex/connection/__init__.py +19 -17
- atex/connection/podman.py +2 -13
- atex/connection/ssh.py +39 -43
- atex/executor/__init__.py +0 -3
- atex/executor/duration.py +1 -1
- atex/executor/executor.py +47 -33
- atex/executor/reporter.py +39 -27
- atex/executor/scripts.py +8 -8
- atex/executor/testcontrol.py +46 -34
- atex/fmf.py +28 -44
- atex/orchestrator/__init__.py +6 -5
- atex/orchestrator/adhoc.py +59 -60
- atex/orchestrator/contest.py +15 -11
- atex/provisioner/__init__.py +31 -22
- atex/provisioner/libvirt/libvirt.py +50 -44
- atex/provisioner/libvirt/locking.py +25 -23
- atex/provisioner/podman/podman.py +8 -8
- atex/provisioner/testingfarm/api.py +91 -77
- atex/provisioner/testingfarm/testingfarm.py +16 -12
- atex/util/__init__.py +23 -0
- atex/util/dedent.py +2 -2
- atex/util/named_mapping.py +3 -3
- atex/util/path.py +1 -1
- atex/util/subprocess.py +28 -22
- atex/util/threads.py +9 -9
- {atex-0.13.dist-info → atex-0.15.dist-info}/METADATA +1 -1
- atex-0.15.dist-info/RECORD +44 -0
- atex/util/log.py +0 -71
- atex-0.13.dist-info/RECORD +0 -45
- {atex-0.13.dist-info → atex-0.15.dist-info}/WHEEL +0 -0
- {atex-0.13.dist-info → atex-0.15.dist-info}/entry_points.txt +0 -0
- {atex-0.13.dist-info → atex-0.15.dist-info}/licenses/COPYING.txt +0 -0
|
@@ -4,6 +4,7 @@ import uuid
|
|
|
4
4
|
import shlex
|
|
5
5
|
import socket
|
|
6
6
|
import random
|
|
7
|
+
import logging
|
|
7
8
|
import textwrap
|
|
8
9
|
import tempfile
|
|
9
10
|
import threading
|
|
@@ -18,6 +19,8 @@ from . import locking
|
|
|
18
19
|
|
|
19
20
|
libvirt = util.import_libvirt()
|
|
20
21
|
|
|
22
|
+
logger = logging.getLogger("atex.provisioner.libvirt")
|
|
23
|
+
|
|
21
24
|
# thread-safe bool
|
|
22
25
|
libvirt_needs_setup = threading.Semaphore(1)
|
|
23
26
|
|
|
@@ -34,7 +37,7 @@ def setup_event_loop():
|
|
|
34
37
|
time.sleep(0.5)
|
|
35
38
|
libvirt.virEventRunDefaultImpl()
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
logger.debug("starting libvirt event loop")
|
|
38
41
|
thread = threading.Thread(target=loop, name="libvirt_event_loop", daemon=True)
|
|
39
42
|
thread.start()
|
|
40
43
|
|
|
@@ -46,17 +49,17 @@ class LibvirtCloningRemote(Remote, connection.ssh.ManagedSSHConnection):
|
|
|
46
49
|
|
|
47
50
|
def __init__(self, ssh_options, host, domain, source_image, *, release_hook):
|
|
48
51
|
"""
|
|
49
|
-
|
|
52
|
+
- `ssh_options` are a dict, passed to ManagedSSHConnection `__init__()`.
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
- `host` is a str of libvirt host name (used for `repr()`).
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
- `domain` is a str of libvirt domain name (used for `repr()`).
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
- `source_image` is a str of libvirt volume name that was cloned
|
|
59
|
+
for the domain to boot from (used for `repr()`).
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
- `release_hook` is a callable called on `.release()` in addition
|
|
62
|
+
to disconnecting the connection.
|
|
60
63
|
"""
|
|
61
64
|
# NOTE: self.lock inherited from ManagedSSHConnection
|
|
62
65
|
super().__init__(options=ssh_options)
|
|
@@ -129,37 +132,42 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
129
132
|
reserve_delay=3, reserve_time=3600, start_event_loop=True,
|
|
130
133
|
):
|
|
131
134
|
"""
|
|
132
|
-
|
|
135
|
+
- `host` is a ManagedSSHConnection class instance, connected to
|
|
136
|
+
a libvirt host.
|
|
137
|
+
|
|
138
|
+
- `image` is a string with a libvirt storage volume name inside the
|
|
139
|
+
given storage `pool` that should be used as the source for cloning.
|
|
140
|
+
|
|
141
|
+
- `pool` is a libvirt storage pool used by all relevant domains on the
|
|
142
|
+
libvirt host **as well as** the would-be-cloned images.
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
- `domain_filter` is a regex string matching libvirt domain names to
|
|
145
|
+
attempt reservation on. Useful for including only ie. `auto-.*`
|
|
146
|
+
domains while leaving other domains on the same libvirt host
|
|
147
|
+
untouched.
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
- `domain_user` and `domain_sshkey` (strings) specify how to connect to
|
|
150
|
+
an OS booted from the pre-instaled `image`, as these credentials are
|
|
151
|
+
known only to the logic that created the `image` in the first place.
|
|
139
152
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
while leaving other domains on the same libvirt host untouched.
|
|
153
|
+
- `reserve_delay` is an int of how many seconds to wait between trying
|
|
154
|
+
to lock libvirt domains, after every unsuccessful locking attempt.
|
|
143
155
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
known only to the logic that created the 'image' in the first place.
|
|
156
|
+
Ie. with `delay=5` and 20 domains, the code will try to lock every
|
|
157
|
+
domain in `5*20=100` seconds before looping back to the first.
|
|
147
158
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
159
|
+
- `reserve_time` is an int of maximum seconds to reserve a libvirt
|
|
160
|
+
domain for before other users can steal it for themselves. Note that
|
|
161
|
+
there is no automatic timeout release logic, it's just a hint for
|
|
162
|
+
others.
|
|
152
163
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
- `start_event_loop` set to `True` starts a global default libvirt event
|
|
165
|
+
loop as part of `.start()` (or context manager enter) in a background
|
|
166
|
+
daemon thread.
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
This is necessary to maintain connection keep-alives, but if you plan
|
|
161
|
-
on managing the loop yourself (have custom uses for the libvirt module),
|
|
162
|
-
setting False here avoids any meddling by this class.
|
|
168
|
+
This is necessary to maintain connection keep-alives, but if you plan
|
|
169
|
+
on managing the loop yourself (have custom uses for the libvirt
|
|
170
|
+
module), setting `False` here avoids any meddling by this class.
|
|
163
171
|
"""
|
|
164
172
|
self.lock = threading.RLock()
|
|
165
173
|
self.host = host
|
|
@@ -204,7 +212,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
204
212
|
xml_root = ET.fromstring(source_vol.XMLDesc())
|
|
205
213
|
source_format = xml_root.find("target").find("format").get("type")
|
|
206
214
|
|
|
207
|
-
|
|
215
|
+
logger.info(
|
|
208
216
|
f"found volume {source_vol.name()} (format:{source_format}) in pool {pool.name()}",
|
|
209
217
|
)
|
|
210
218
|
|
|
@@ -214,7 +222,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
214
222
|
already_reserving = {conn.lookupByName(name) for name in already_reserving}
|
|
215
223
|
|
|
216
224
|
# acquire (lock) a domain on the libvirt host
|
|
217
|
-
|
|
225
|
+
logger.info("attempting to acquire a domain")
|
|
218
226
|
acquired = None
|
|
219
227
|
while not acquired:
|
|
220
228
|
domains = []
|
|
@@ -229,7 +237,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
229
237
|
for domain in domains:
|
|
230
238
|
if locking.lock(domain, self.signature, self.reserve_end):
|
|
231
239
|
acquired = domain
|
|
232
|
-
|
|
240
|
+
logger.info(f"acquired domain {acquired.name()}")
|
|
233
241
|
break
|
|
234
242
|
time.sleep(self.reserve_delay)
|
|
235
243
|
|
|
@@ -245,7 +253,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
245
253
|
|
|
246
254
|
# parse XML definition of the domain
|
|
247
255
|
xmldesc = acquired.XMLDesc().rstrip("\n")
|
|
248
|
-
|
|
256
|
+
logger.debug(f"domain {acquired.name()} XML:\n{textwrap.indent(xmldesc, ' ')}")
|
|
249
257
|
xml_root = ET.fromstring(xmldesc)
|
|
250
258
|
nvram_vol = nvram_path = None
|
|
251
259
|
|
|
@@ -279,7 +287,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
279
287
|
if "Storage volume not found" not in str(e):
|
|
280
288
|
raise
|
|
281
289
|
if nvram_vol:
|
|
282
|
-
|
|
290
|
+
logger.info(f"deleting nvram volume {nvram_vol.name()}")
|
|
283
291
|
nvram_vol.delete()
|
|
284
292
|
|
|
285
293
|
# try to find a disk that is a volume in the specified storage pool
|
|
@@ -298,7 +306,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
298
306
|
if xml_disk_source.get("pool") != pool.name():
|
|
299
307
|
continue
|
|
300
308
|
disk_vol_name = xml_disk_source.get("volume")
|
|
301
|
-
|
|
309
|
+
logger.info(f"found a domain disk in XML: {disk_vol_name} for pool {pool.name()}")
|
|
302
310
|
break
|
|
303
311
|
else:
|
|
304
312
|
raise RuntimeError("could not find any <disk> in <devices>")
|
|
@@ -322,7 +330,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
322
330
|
pool.createXMLFrom(new_volume, source_vol)
|
|
323
331
|
|
|
324
332
|
# start the domain up
|
|
325
|
-
|
|
333
|
+
logger.info(f"starting up {acquired.name()}")
|
|
326
334
|
acquired.create() # like 'virsh start' NOT 'virsh create'
|
|
327
335
|
|
|
328
336
|
# wait for an IP address leased by libvirt host
|
|
@@ -332,7 +340,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
332
340
|
libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE,
|
|
333
341
|
)
|
|
334
342
|
time.sleep(1)
|
|
335
|
-
|
|
343
|
+
logger.info(f"found iface addrs: {addrs}")
|
|
336
344
|
first_iface = next(iter(addrs.values()))
|
|
337
345
|
first_addr = next(iter(first_iface.values()))[0]["addr"]
|
|
338
346
|
|
|
@@ -414,7 +422,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
414
422
|
name = Path(f.name)
|
|
415
423
|
name.chmod(0o0500) # r-x------
|
|
416
424
|
uri = f"qemu+ext:///system?command={urllib.parse.quote(str(name.absolute()))}"
|
|
417
|
-
|
|
425
|
+
logger.info(f"opening libvirt conn to {uri}")
|
|
418
426
|
conn = libvirt.open(uri)
|
|
419
427
|
conn.setKeepAlive(5, 3)
|
|
420
428
|
return conn
|
|
@@ -429,7 +437,6 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
429
437
|
|
|
430
438
|
def stop(self):
|
|
431
439
|
with self.lock:
|
|
432
|
-
#util.debug(f"SELF.RESERVING: {self.reserving} // SELF.REMOTES: {self.remotes}")
|
|
433
440
|
# close reserving libvirt host connection
|
|
434
441
|
# - this stops _reserve_one() from doing anything bad
|
|
435
442
|
if self.reserve_conn:
|
|
@@ -445,8 +452,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
445
452
|
try:
|
|
446
453
|
domain = self.manage_conn.lookupByName(self.reserving.pop())
|
|
447
454
|
locking.unlock(domain, self.signature)
|
|
448
|
-
except libvirt.libvirtError
|
|
449
|
-
util.debug(f"GOT ERROR: {str(e)}")
|
|
455
|
+
except libvirt.libvirtError:
|
|
450
456
|
pass
|
|
451
457
|
# cancel/release all Remotes ever created by us
|
|
452
458
|
while self.remotes:
|
|
@@ -9,8 +9,8 @@ Lock safety is ensured by libvirt retaining <metadata> content (tag) order,
|
|
|
9
9
|
so each user can re-check (after locking) whether the race was won or lost,
|
|
10
10
|
depending on whether the user's signature is on top of <metadata>.
|
|
11
11
|
|
|
12
|
-
A timestamp (meaning now()+duration) is used as the lock tag value/text,
|
|
13
|
-
and any expired timestamps (now() > timestamp) are ignored by the locking
|
|
12
|
+
A timestamp (meaning `now()+duration`) is used as the lock tag value/text,
|
|
13
|
+
and any expired timestamps (`now() > timestamp`) are ignored by the locking
|
|
14
14
|
logic.
|
|
15
15
|
"""
|
|
16
16
|
|
|
@@ -26,11 +26,11 @@ libvirt = util.import_libvirt()
|
|
|
26
26
|
|
|
27
27
|
def get_locks(domain, expired=False):
|
|
28
28
|
"""
|
|
29
|
-
Yield (signature,timestamp) tuples of atex-lock entries for a
|
|
29
|
+
Yield (signature,timestamp) tuples of atex-lock entries for a `domain`.
|
|
30
30
|
|
|
31
|
-
If
|
|
31
|
+
- If `expired` is `True`, yield also locks with an expired timestamp.
|
|
32
32
|
|
|
33
|
-
If a timestamp is missing, it is substituted with 0
|
|
33
|
+
If a timestamp is missing, it is substituted with `0`.
|
|
34
34
|
"""
|
|
35
35
|
xml_dump = ET.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE))
|
|
36
36
|
metadata = xml_dump.find("metadata")
|
|
@@ -49,7 +49,7 @@ def get_locks(domain, expired=False):
|
|
|
49
49
|
|
|
50
50
|
def unlock(domain, signature):
|
|
51
51
|
"""
|
|
52
|
-
Unlock a domain previously locked by lock()
|
|
52
|
+
Unlock a domain previously locked by `lock()`.
|
|
53
53
|
"""
|
|
54
54
|
domain.setMetadata(
|
|
55
55
|
libvirt.VIR_DOMAIN_METADATA_ELEMENT,
|
|
@@ -62,10 +62,10 @@ def unlock(domain, signature):
|
|
|
62
62
|
|
|
63
63
|
def lock(domain, signature, timestamp):
|
|
64
64
|
"""
|
|
65
|
-
Attempt to lock a domain under
|
|
66
|
-
writing out
|
|
65
|
+
Attempt to lock a domain under `signature` ownership,
|
|
66
|
+
writing out `timestamp` as the lock tag content.
|
|
67
67
|
|
|
68
|
-
Returns True if the domain was successfully locked, False otherwise.
|
|
68
|
+
Returns `True` if the domain was successfully locked, `False` otherwise.
|
|
69
69
|
"""
|
|
70
70
|
signature = str(signature)
|
|
71
71
|
timestamp = int(timestamp)
|
|
@@ -102,17 +102,19 @@ def lock(domain, signature, timestamp):
|
|
|
102
102
|
|
|
103
103
|
def lock_any(connection, signature, duration, filter_domains=lambda _: True):
|
|
104
104
|
"""
|
|
105
|
-
Given a libvirt
|
|
106
|
-
domain under
|
|
105
|
+
Given a libvirt `connection`, attempt to lock (reserve) any one
|
|
106
|
+
domain under `signature` ownership for `duration` seconds.
|
|
107
107
|
|
|
108
|
-
If
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
- If `filter_domain` is given as a callable, it is used to filter
|
|
109
|
+
domains considered for locking.
|
|
110
|
+
|
|
111
|
+
It takes one argument (libvirt domain object) and must return `True`
|
|
112
|
+
(domain is eligible for locking) or `False` (domain should be skipped).
|
|
113
|
+
|
|
114
|
+
For example: `lambda dom: dom.name().startswith("foo-")`
|
|
113
115
|
|
|
114
116
|
Returns a libvirt domain object of a successfully locked domain,
|
|
115
|
-
or None if no domain could be locked.
|
|
117
|
+
or `None` if no domain could be locked.
|
|
116
118
|
"""
|
|
117
119
|
domains = connection.listAllDomains(libvirt.VIR_CONNECT_LIST_DOMAINS_PERSISTENT)
|
|
118
120
|
# try to avoid lock conflicts
|
|
@@ -129,11 +131,11 @@ def unlock_all(connection, signature=None, shutdown=False, filter_domains=lambda
|
|
|
129
131
|
"""
|
|
130
132
|
Remove all locks for all domains.
|
|
131
133
|
|
|
132
|
-
If
|
|
134
|
+
- If `signature` is given, remove only locks matching the signature.
|
|
133
135
|
|
|
134
|
-
If
|
|
136
|
+
- If `shutdown` is `True`, also forcibly shutdown (destroy) all domains.
|
|
135
137
|
|
|
136
|
-
If
|
|
138
|
+
- If `filter_domains` is given, it behaves like for `lock_any()`.
|
|
137
139
|
"""
|
|
138
140
|
domains = connection.listAllDomains(libvirt.VIR_CONNECT_LIST_DOMAINS_PERSISTENT)
|
|
139
141
|
for domain in filter(filter_domains, domains):
|
|
@@ -156,11 +158,11 @@ def cleanup_expired(connection, timestamp=None, filter_domains=lambda _: True):
|
|
|
156
158
|
for the given signature, it is never removed, unless this function is used
|
|
157
159
|
(by some maintenance service).
|
|
158
160
|
|
|
159
|
-
Note that unlock_all() cleans up all locks, incl. expired ones.
|
|
161
|
+
Note that `unlock_all()` cleans up all locks, incl. expired ones.
|
|
160
162
|
|
|
161
|
-
If
|
|
163
|
+
- If `timestamp` is given, it is used instead of the `now()` UTC timestamp.
|
|
162
164
|
|
|
163
|
-
If
|
|
165
|
+
- If `filter_domains` is given, it behaves like for `lock_any()`.
|
|
164
166
|
"""
|
|
165
167
|
now = int(timestamp) if timestamp is not None else int(time.time())
|
|
166
168
|
domains = connection.listAllDomains(libvirt.VIR_CONNECT_LIST_DOMAINS_PERSISTENT)
|
|
@@ -14,12 +14,12 @@ class PodmanRemote(Remote, connection.podman.PodmanConnection):
|
|
|
14
14
|
|
|
15
15
|
def __init__(self, image, container, *, release_hook):
|
|
16
16
|
"""
|
|
17
|
-
|
|
17
|
+
- `image` is an image tag (used for `repr()`).
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
- `container` is a podman container ID / name.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
- `release_hook` is a callable called on `.release()` in addition
|
|
22
|
+
to disconnecting the connection.
|
|
23
23
|
"""
|
|
24
24
|
super().__init__(container=container)
|
|
25
25
|
self.lock = threading.RLock()
|
|
@@ -61,11 +61,11 @@ class PodmanRemote(Remote, connection.podman.PodmanConnection):
|
|
|
61
61
|
class PodmanProvisioner(Provisioner):
|
|
62
62
|
def __init__(self, image, run_options=None):
|
|
63
63
|
"""
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
- `image` is a string of image tag/ID to create containers from.
|
|
65
|
+
It can be a local identifier or an URL.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
- `run_options` is an iterable with additional CLI options passed
|
|
68
|
+
to `podman container run`.
|
|
69
69
|
"""
|
|
70
70
|
self.lock = threading.RLock()
|
|
71
71
|
self.image = image
|