atex 0.9__py3-none-any.whl → 0.11__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/aggregator/__init__.py +62 -0
- atex/aggregator/json.py +279 -0
- atex/cli/__init__.py +14 -1
- atex/cli/fmf.py +7 -7
- atex/cli/libvirt.py +3 -2
- atex/cli/testingfarm.py +74 -3
- atex/connection/podman.py +2 -4
- atex/connection/ssh.py +7 -14
- atex/executor/executor.py +21 -20
- atex/executor/scripts.py +5 -3
- atex/executor/testcontrol.py +1 -1
- atex/orchestrator/__init__.py +76 -3
- atex/orchestrator/{orchestrator.py → adhoc.py} +246 -108
- atex/orchestrator/contest.py +94 -0
- atex/{provision → provisioner}/__init__.py +48 -52
- atex/{provision → provisioner}/libvirt/libvirt.py +34 -15
- atex/{provision → provisioner}/libvirt/locking.py +3 -1
- atex/provisioner/podman/__init__.py +2 -0
- atex/provisioner/podman/podman.py +169 -0
- atex/{provision → provisioner}/testingfarm/api.py +56 -48
- atex/{provision → provisioner}/testingfarm/testingfarm.py +43 -45
- atex/util/log.py +62 -67
- atex/util/subprocess.py +46 -12
- atex/util/threads.py +7 -0
- atex-0.11.dist-info/METADATA +86 -0
- atex-0.11.dist-info/RECORD +45 -0
- {atex-0.9.dist-info → atex-0.11.dist-info}/WHEEL +1 -1
- atex/orchestrator/aggregator.py +0 -111
- atex/provision/podman/__init__.py +0 -1
- atex/provision/podman/podman.py +0 -274
- atex-0.9.dist-info/METADATA +0 -178
- atex-0.9.dist-info/RECORD +0 -43
- /atex/{provision → provisioner}/libvirt/VM_PROVISION +0 -0
- /atex/{provision → provisioner}/libvirt/__init__.py +0 -0
- /atex/{provision → provisioner}/libvirt/setup-libvirt.sh +0 -0
- /atex/{provision → provisioner}/testingfarm/__init__.py +0 -0
- {atex-0.9.dist-info → atex-0.11.dist-info}/entry_points.txt +0 -0
- {atex-0.9.dist-info → atex-0.11.dist-info}/licenses/COPYING.txt +0 -0
|
@@ -4,41 +4,74 @@ import pkgutil as _pkgutil
|
|
|
4
4
|
from .. import connection as _connection
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
class Remote(_connection.Connection):
|
|
8
|
+
"""
|
|
9
|
+
Representation of a provisioned (reserved) remote system, providing
|
|
10
|
+
a Connection-like API in addition to system management helpers.
|
|
11
|
+
|
|
12
|
+
An instance of Remote is typically prepared by a Provisioner and returned
|
|
13
|
+
to the caller for use and an eventual .release().
|
|
14
|
+
|
|
15
|
+
Also note that Remote can be used via Context Manager, but does not
|
|
16
|
+
do automatic .release(), the manager only handles the built-in Connection.
|
|
17
|
+
The intention is for a Provisioner to run via its own Contest Manager and
|
|
18
|
+
release all Remotes upon exit.
|
|
19
|
+
If you need automatic release of one Remote, use a try/finally block.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def release(self):
|
|
23
|
+
"""
|
|
24
|
+
Release (de-provision) the remote resource.
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError(f"'release' not implemented for {self.__class__.__name__}")
|
|
27
|
+
|
|
28
|
+
|
|
7
29
|
class Provisioner:
|
|
8
30
|
"""
|
|
9
31
|
A remote resource (machine/system) provider.
|
|
10
32
|
|
|
11
|
-
The
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
The idea is to request machines (a.k.a. Remotes, or class Remote instances)
|
|
34
|
+
to be reserved via a non-blocking .provision() and for them to be retrieved
|
|
35
|
+
through blocking / non-blocking .get_remote() when they become available.
|
|
36
|
+
|
|
37
|
+
Each Remote has its own .release() for freeing (de-provisioning) it once
|
|
38
|
+
the user doesn't need it anymore. The Provisioner does this automatically
|
|
39
|
+
to all Remotes during .stop() or context manager exit.
|
|
15
40
|
|
|
16
41
|
p = Provisioner()
|
|
17
42
|
p.start()
|
|
43
|
+
p.provision(count=1)
|
|
18
44
|
remote = p.get_remote()
|
|
19
45
|
remote.cmd(["ls", "/"])
|
|
20
46
|
remote.release()
|
|
21
47
|
p.stop()
|
|
22
48
|
|
|
23
49
|
with Provisioner() as p:
|
|
24
|
-
|
|
50
|
+
p.provision(count=2)
|
|
51
|
+
remote1 = p.get_remote()
|
|
52
|
+
remote2 = p.get_remote()
|
|
25
53
|
...
|
|
26
|
-
remote.release()
|
|
27
|
-
|
|
28
|
-
TODO: mention how a Provisioner always needs to take care of release all Remotes
|
|
29
|
-
when .stop()ped or when context terminates; even the ones handed over to
|
|
30
|
-
the user
|
|
31
54
|
|
|
32
|
-
Note that .
|
|
33
|
-
|
|
55
|
+
Note that .provision() is a hint expressed by the caller, not a guarantee
|
|
56
|
+
that .get_remote() will ever return a Remote. Ie. the caller can call
|
|
57
|
+
.provision(count=math.inf) to receive as many remotes as the Provisioner
|
|
58
|
+
can possibly supply.
|
|
34
59
|
"""
|
|
35
60
|
|
|
61
|
+
def provision(self, count=1):
|
|
62
|
+
"""
|
|
63
|
+
Request that 'count' machines be provisioned (reserved) for use,
|
|
64
|
+
to be returned at a later point by .get_remote().
|
|
65
|
+
"""
|
|
66
|
+
raise NotImplementedError(f"'provision' not implemented for {self.__class__.__name__}")
|
|
67
|
+
|
|
36
68
|
def get_remote(self, block=True):
|
|
37
69
|
"""
|
|
38
|
-
|
|
70
|
+
Return a connected class Remote instance of a previously .provision()ed
|
|
71
|
+
remote system.
|
|
39
72
|
|
|
40
|
-
If 'block' is True, wait for the
|
|
41
|
-
otherwise return None if there is
|
|
73
|
+
If 'block' is True, wait for the Remote to be available and connected,
|
|
74
|
+
otherwise return None if there is none available yet.
|
|
42
75
|
"""
|
|
43
76
|
raise NotImplementedError(f"'get_remote' not implemented for {self.__class__.__name__}")
|
|
44
77
|
|
|
@@ -56,18 +89,6 @@ class Provisioner:
|
|
|
56
89
|
"""
|
|
57
90
|
raise NotImplementedError(f"'stop' not implemented for {self.__class__.__name__}")
|
|
58
91
|
|
|
59
|
-
def stop_defer(self):
|
|
60
|
-
"""
|
|
61
|
-
Enable an external caller to stop the Provisioner instance,
|
|
62
|
-
deferring resource deallocation to the caller.
|
|
63
|
-
|
|
64
|
-
Return an iterable of argument-free thread-safe callables that can be
|
|
65
|
-
called, possibly in parallel, to free up resources.
|
|
66
|
-
Ie. a list of 200 .release() functions, to be called in a thread pool
|
|
67
|
-
by the user, speeding up cleanup.
|
|
68
|
-
"""
|
|
69
|
-
return (self.stop,)
|
|
70
|
-
|
|
71
92
|
def __enter__(self):
|
|
72
93
|
try:
|
|
73
94
|
self.start()
|
|
@@ -80,31 +101,6 @@ class Provisioner:
|
|
|
80
101
|
self.stop()
|
|
81
102
|
|
|
82
103
|
|
|
83
|
-
class Remote(_connection.Connection):
|
|
84
|
-
"""
|
|
85
|
-
Representation of a provisioned (reserved) remote system, providing
|
|
86
|
-
a Connection-like API in addition to system management helpers.
|
|
87
|
-
|
|
88
|
-
An instance of Remote is typically prepared by a Provisioner and lent out
|
|
89
|
-
for further use, to be .release()d by the user (if destroyed).
|
|
90
|
-
It is not meant for repeated reserve/release cycles, hence the lack
|
|
91
|
-
of .reserve().
|
|
92
|
-
|
|
93
|
-
Also note that Remote can be used via Context Manager, but does not
|
|
94
|
-
do automatic .release(), the manager only handles the built-in Connection.
|
|
95
|
-
The intention is for a Provisioner to run via its own Contest Manager and
|
|
96
|
-
release all Remotes upon exit.
|
|
97
|
-
If you need automatic release of one Remote, use a contextlib.ExitStack
|
|
98
|
-
with a callback, or a try/finally block.
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
def release(self):
|
|
102
|
-
"""
|
|
103
|
-
Release (de-provision) the remote resource.
|
|
104
|
-
"""
|
|
105
|
-
raise NotImplementedError(f"'release' not implemented for {self.__class__.__name__}")
|
|
106
|
-
|
|
107
|
-
|
|
108
104
|
_submodules = [
|
|
109
105
|
info.name for info in _pkgutil.iter_modules(__spec__.submodule_search_locations)
|
|
110
106
|
]
|
|
@@ -4,6 +4,7 @@ import uuid
|
|
|
4
4
|
import shlex
|
|
5
5
|
import socket
|
|
6
6
|
import random
|
|
7
|
+
import textwrap
|
|
7
8
|
import tempfile
|
|
8
9
|
import threading
|
|
9
10
|
import subprocess
|
|
@@ -38,14 +39,14 @@ def setup_event_loop():
|
|
|
38
39
|
thread.start()
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
class LibvirtCloningRemote(Remote, connection.ssh.
|
|
42
|
+
class LibvirtCloningRemote(Remote, connection.ssh.ManagedSSHConnection):
|
|
42
43
|
"""
|
|
43
44
|
TODO
|
|
44
45
|
"""
|
|
45
46
|
|
|
46
47
|
def __init__(self, ssh_options, host, domain, source_image, *, release_hook):
|
|
47
48
|
"""
|
|
48
|
-
'ssh_options' are a dict, passed to
|
|
49
|
+
'ssh_options' are a dict, passed to ManagedSSHConnection __init__().
|
|
49
50
|
|
|
50
51
|
'host' is a str of libvirt host name (used for repr()).
|
|
51
52
|
|
|
@@ -57,7 +58,7 @@ class LibvirtCloningRemote(Remote, connection.ssh.ManagedSSHConn):
|
|
|
57
58
|
'release_hook' is a callable called on .release() in addition
|
|
58
59
|
to disconnecting the connection.
|
|
59
60
|
"""
|
|
60
|
-
# NOTE: self.lock inherited from
|
|
61
|
+
# NOTE: self.lock inherited from ManagedSSHConnection
|
|
61
62
|
super().__init__(options=ssh_options)
|
|
62
63
|
self.host = host
|
|
63
64
|
self.domain = domain
|
|
@@ -80,7 +81,7 @@ class LibvirtCloningRemote(Remote, connection.ssh.ManagedSSHConn):
|
|
|
80
81
|
return f"{class_name}({self.host}, {self.domain}, {self.source_image})"
|
|
81
82
|
|
|
82
83
|
|
|
83
|
-
# needs
|
|
84
|
+
# needs ManagedSSHConnection due to .forward()
|
|
84
85
|
def reliable_ssh_local_fwd(conn, dest, retries=10):
|
|
85
86
|
for _ in range(retries):
|
|
86
87
|
# let the kernel give us a free port
|
|
@@ -128,7 +129,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
128
129
|
reserve_delay=3, reserve_time=3600, start_event_loop=True,
|
|
129
130
|
):
|
|
130
131
|
"""
|
|
131
|
-
'host' is a
|
|
132
|
+
'host' is a ManagedSSHConnection class instance, connected to a libvirt host.
|
|
132
133
|
|
|
133
134
|
'image' is a string with a libvirt storage volume name inside the
|
|
134
135
|
given storage 'pool' that should be used as the source for cloning.
|
|
@@ -174,6 +175,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
174
175
|
self.signature = uuid.uuid4()
|
|
175
176
|
self.reserve_end = None
|
|
176
177
|
self.queue = util.ThreadQueue(daemon=True)
|
|
178
|
+
self.to_reserve = 0
|
|
177
179
|
|
|
178
180
|
# use two libvirt connections - one to handle reservations and cloning,
|
|
179
181
|
# and another for management and cleanup;
|
|
@@ -242,8 +244,8 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
242
244
|
raise
|
|
243
245
|
|
|
244
246
|
# parse XML definition of the domain
|
|
245
|
-
xmldesc = acquired.XMLDesc()
|
|
246
|
-
util.
|
|
247
|
+
xmldesc = acquired.XMLDesc().rstrip("\n")
|
|
248
|
+
util.extradebug(f"domain {acquired.name()} XML:\n{textwrap.indent(xmldesc, ' ')}")
|
|
247
249
|
xml_root = ET.fromstring(xmldesc)
|
|
248
250
|
nvram_vol = nvram_path = None
|
|
249
251
|
|
|
@@ -258,7 +260,19 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
258
260
|
# by libvirt natively (because treating nvram as a storage pool
|
|
259
261
|
# is a user hack)
|
|
260
262
|
for p in conn.listAllStoragePools():
|
|
261
|
-
|
|
263
|
+
# retry a few times to work around a libvirt race condition
|
|
264
|
+
for _ in range(10):
|
|
265
|
+
try:
|
|
266
|
+
p.refresh()
|
|
267
|
+
except libvirt.libvirtError as e:
|
|
268
|
+
if "domain is not running" in str(e):
|
|
269
|
+
break
|
|
270
|
+
elif "has asynchronous jobs running" in str(e):
|
|
271
|
+
continue
|
|
272
|
+
else:
|
|
273
|
+
raise
|
|
274
|
+
else:
|
|
275
|
+
break
|
|
262
276
|
try:
|
|
263
277
|
nvram_vol = conn.storageVolLookupByPath(nvram_path)
|
|
264
278
|
except libvirt.libvirtError as e:
|
|
@@ -325,7 +339,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
325
339
|
# set up ssh LocalForward to it
|
|
326
340
|
port = reliable_ssh_local_fwd(self.host, f"{first_addr}:22")
|
|
327
341
|
|
|
328
|
-
#
|
|
342
|
+
# prepare release using variables from this scope
|
|
329
343
|
def release_hook(remote):
|
|
330
344
|
# un-forward the libvirt host ssh-forwarded port
|
|
331
345
|
self.host.forward("LocalForward", f"127.0.0.1:{port} {first_addr}:22", cancel=True)
|
|
@@ -339,6 +353,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
339
353
|
try:
|
|
340
354
|
domain = self.manage_conn.lookupByName(remote.domain)
|
|
341
355
|
locking.unlock(domain, self.signature)
|
|
356
|
+
domain.destroy()
|
|
342
357
|
except libvirt.libvirtError as e:
|
|
343
358
|
if "Domain not found" not in str(e):
|
|
344
359
|
raise
|
|
@@ -348,11 +363,12 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
348
363
|
except ValueError:
|
|
349
364
|
pass
|
|
350
365
|
|
|
366
|
+
# create a remote and connect it
|
|
351
367
|
ssh_options = {
|
|
352
368
|
"Hostname": "127.0.0.1",
|
|
353
369
|
"User": self.domain_user,
|
|
354
370
|
"Port": str(port),
|
|
355
|
-
"IdentityFile": self.domain_sshkey,
|
|
371
|
+
"IdentityFile": str(Path(self.domain_sshkey).absolute()),
|
|
356
372
|
"ConnectionAttempts": "1000",
|
|
357
373
|
"Compression": "yes",
|
|
358
374
|
}
|
|
@@ -385,7 +401,7 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
385
401
|
("virt-ssh-helper", "qemu:///system"),
|
|
386
402
|
func=lambda *args, **_: args[0],
|
|
387
403
|
)
|
|
388
|
-
# to make libvirt connect via our
|
|
404
|
+
# to make libvirt connect via our ManagedSSHConnection, we need to give it
|
|
389
405
|
# a specific ssh CLI, but libvirt URI command= takes only one argv[0]
|
|
390
406
|
# and cannot pass arguments - we work around this by creating a temp
|
|
391
407
|
# arg-less executable
|
|
@@ -410,8 +426,6 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
410
426
|
self.reserve_conn = self._open_libvirt_conn()
|
|
411
427
|
self.manage_conn = self.reserve_conn # for now
|
|
412
428
|
self.reserve_end = int(time.time()) + self.reserve_time
|
|
413
|
-
# get an initial first remote
|
|
414
|
-
self.queue.start_thread(target=self._reserve_one)
|
|
415
429
|
|
|
416
430
|
def stop(self):
|
|
417
431
|
with self.lock:
|
|
@@ -443,11 +457,16 @@ class LibvirtCloningProvisioner(Provisioner):
|
|
|
443
457
|
self.reserve_end = None
|
|
444
458
|
# TODO: wait for threadqueue threads to join?
|
|
445
459
|
|
|
460
|
+
def provision(self, count=1):
|
|
461
|
+
with self.lock:
|
|
462
|
+
self.to_reserve += count
|
|
463
|
+
|
|
446
464
|
def get_remote(self, block=True):
|
|
447
|
-
# if the reservation thread is not running, start one
|
|
448
465
|
with self.lock:
|
|
449
|
-
if not
|
|
466
|
+
# if the reservation thread is not running, start one
|
|
467
|
+
if not self.queue.threads and self.to_reserve > 0:
|
|
450
468
|
self.queue.start_thread(target=self._reserve_one)
|
|
469
|
+
self.to_reserve -= 1
|
|
451
470
|
try:
|
|
452
471
|
return self.queue.get(block=block)
|
|
453
472
|
except util.ThreadQueue.Empty:
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import threading
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from ... import connection, util
|
|
6
|
+
from .. import Provisioner, Remote
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PodmanRemote(Remote, connection.podman.PodmanConnection):
|
|
10
|
+
"""
|
|
11
|
+
Built on the official Remote API, pulling in the Connection API
|
|
12
|
+
as implemented by ManagedSSHConnection.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, image, container, *, release_hook):
|
|
16
|
+
"""
|
|
17
|
+
'image' is an image tag (used for repr()).
|
|
18
|
+
|
|
19
|
+
'container' is a podman container id / name.
|
|
20
|
+
|
|
21
|
+
'release_hook' is a callable called on .release() in addition
|
|
22
|
+
to disconnecting the connection.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(container=container)
|
|
25
|
+
self.lock = threading.RLock()
|
|
26
|
+
self.image = image
|
|
27
|
+
self.container = container
|
|
28
|
+
self.release_called = False
|
|
29
|
+
self.release_hook = release_hook
|
|
30
|
+
|
|
31
|
+
def release(self):
|
|
32
|
+
with self.lock:
|
|
33
|
+
if self.release_called:
|
|
34
|
+
return
|
|
35
|
+
else:
|
|
36
|
+
self.release_called = True
|
|
37
|
+
self.release_hook(self)
|
|
38
|
+
self.disconnect()
|
|
39
|
+
util.subprocess_run(
|
|
40
|
+
("podman", "container", "rm", "-f", "-t", "0", self.container),
|
|
41
|
+
check=False, # ignore if it fails
|
|
42
|
+
stdout=subprocess.DEVNULL,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# not /technically/ a valid repr(), but meh
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
class_name = self.__class__.__name__
|
|
48
|
+
|
|
49
|
+
if "/" in self.image:
|
|
50
|
+
image = self.image.rsplit("/",1)[1]
|
|
51
|
+
elif len(self.image) > 20:
|
|
52
|
+
image = f"{self.image[:17]}..."
|
|
53
|
+
else:
|
|
54
|
+
image = self.image
|
|
55
|
+
|
|
56
|
+
name = f"{self.container[:17]}..." if len(self.container) > 20 else self.container
|
|
57
|
+
|
|
58
|
+
return f"{class_name}({image}, {name})"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PodmanProvisioner(Provisioner):
|
|
62
|
+
def __init__(self, image, run_options=None):
|
|
63
|
+
"""
|
|
64
|
+
'image' is a string of image tag/id to create containers from.
|
|
65
|
+
It can be a local identifier or an URL.
|
|
66
|
+
|
|
67
|
+
'run_options' is an iterable with additional CLI options passed
|
|
68
|
+
to 'podman container run'.
|
|
69
|
+
"""
|
|
70
|
+
self.lock = threading.RLock()
|
|
71
|
+
self.image = image
|
|
72
|
+
self.run_options = run_options or ()
|
|
73
|
+
|
|
74
|
+
# created PodmanRemote instances, ready to be handed over to the user,
|
|
75
|
+
# or already in use by the user
|
|
76
|
+
self.remotes = []
|
|
77
|
+
self.to_create = 0
|
|
78
|
+
|
|
79
|
+
def start(self):
|
|
80
|
+
if not self.image:
|
|
81
|
+
raise ValueError("image cannot be empty")
|
|
82
|
+
|
|
83
|
+
def stop(self):
|
|
84
|
+
with self.lock:
|
|
85
|
+
while self.remotes:
|
|
86
|
+
self.remotes.pop().release()
|
|
87
|
+
|
|
88
|
+
def provision(self, count=1):
|
|
89
|
+
with self.lock:
|
|
90
|
+
self.to_create += count
|
|
91
|
+
|
|
92
|
+
def get_remote(self, block=True):
|
|
93
|
+
if self.to_create <= 0:
|
|
94
|
+
if block:
|
|
95
|
+
raise RuntimeError("no .provision() requested, would block forever")
|
|
96
|
+
else:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
proc = util.subprocess_run(
|
|
100
|
+
(
|
|
101
|
+
"podman", "container", "run", "--quiet", "--detach", "--pull", "never",
|
|
102
|
+
*self.run_options, self.image, "sleep", "inf",
|
|
103
|
+
),
|
|
104
|
+
check=True,
|
|
105
|
+
text=True,
|
|
106
|
+
stdout=subprocess.PIPE,
|
|
107
|
+
)
|
|
108
|
+
container_id = proc.stdout.rstrip("\n")
|
|
109
|
+
|
|
110
|
+
def release_hook(remote):
|
|
111
|
+
# remove from the list of remotes inside this Provisioner
|
|
112
|
+
with self.lock:
|
|
113
|
+
try:
|
|
114
|
+
self.remotes.remove(remote)
|
|
115
|
+
except ValueError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
remote = PodmanRemote(
|
|
119
|
+
self.image,
|
|
120
|
+
container_id,
|
|
121
|
+
release_hook=release_hook,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
with self.lock:
|
|
125
|
+
self.remotes.append(remote)
|
|
126
|
+
self.to_create -= 1
|
|
127
|
+
|
|
128
|
+
return remote
|
|
129
|
+
|
|
130
|
+
# not /technically/ a valid repr(), but meh
|
|
131
|
+
def __repr__(self):
|
|
132
|
+
class_name = self.__class__.__name__
|
|
133
|
+
return (
|
|
134
|
+
f"{class_name}({self.image}, {len(self.remotes)} remotes, {hex(id(self))})"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def pull_image(origin):
|
|
139
|
+
proc = util.subprocess_run(
|
|
140
|
+
("podman", "image", "pull", "-q", origin),
|
|
141
|
+
check=True,
|
|
142
|
+
text=True,
|
|
143
|
+
stdout=subprocess.PIPE,
|
|
144
|
+
)
|
|
145
|
+
return proc.stdout.rstrip("\n")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_container_with_deps(origin, tag=None, *, extra_pkgs=None):
|
|
149
|
+
tag_args = ("-t", tag) if tag else ()
|
|
150
|
+
|
|
151
|
+
pkgs = ["rsync"]
|
|
152
|
+
if extra_pkgs:
|
|
153
|
+
pkgs += extra_pkgs
|
|
154
|
+
pkgs_str = " ".join(pkgs)
|
|
155
|
+
|
|
156
|
+
with tempfile.NamedTemporaryFile("w+t", delete_on_close=False) as tmpf:
|
|
157
|
+
tmpf.write(util.dedent(fr"""
|
|
158
|
+
FROM {origin}
|
|
159
|
+
RUN dnf -y -q --setopt=install_weak_deps=False install {pkgs_str} >/dev/null
|
|
160
|
+
RUN dnf -y -q clean packages >/dev/null
|
|
161
|
+
"""))
|
|
162
|
+
tmpf.close()
|
|
163
|
+
proc = util.subprocess_run(
|
|
164
|
+
("podman", "image", "build", "-q", "-f", tmpf.name, *tag_args, "."),
|
|
165
|
+
check=True,
|
|
166
|
+
text=True,
|
|
167
|
+
stdout=subprocess.PIPE,
|
|
168
|
+
)
|
|
169
|
+
return proc.stdout.rstrip("\n")
|