pyinfra 3.3__py2.py3-none-any.whl → 3.4__py2.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.
- pyinfra/api/arguments.py +8 -16
- pyinfra/api/deploy.py +1 -1
- pyinfra/api/facts.py +10 -26
- pyinfra/api/host.py +10 -4
- pyinfra/api/inventory.py +5 -2
- pyinfra/api/operation.py +1 -1
- pyinfra/api/util.py +20 -6
- pyinfra/connectors/docker.py +117 -38
- pyinfra/connectors/dockerssh.py +1 -0
- pyinfra/connectors/local.py +1 -0
- pyinfra/connectors/ssh.py +1 -0
- pyinfra/connectors/sshuserclient/client.py +5 -5
- pyinfra/connectors/terraform.py +3 -0
- pyinfra/connectors/vagrant.py +3 -0
- pyinfra/context.py +14 -5
- pyinfra/facts/brew.py +1 -0
- pyinfra/facts/docker.py +6 -2
- pyinfra/facts/git.py +10 -0
- pyinfra/facts/hardware.py +1 -1
- pyinfra/facts/opkg.py +1 -0
- pyinfra/facts/server.py +81 -23
- pyinfra/facts/systemd.py +1 -1
- pyinfra/operations/crontab.py +7 -5
- pyinfra/operations/docker.py +2 -0
- pyinfra/operations/files.py +64 -21
- pyinfra/operations/flatpak.py +17 -2
- pyinfra/operations/git.py +6 -2
- pyinfra/operations/server.py +34 -24
- pyinfra/operations/util/docker.py +4 -0
- pyinfra/operations/util/files.py +44 -3
- {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/METADATA +5 -4
- {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/RECORD +47 -47
- {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/entry_points.txt +1 -0
- pyinfra_cli/inventory.py +1 -1
- pyinfra_cli/main.py +4 -2
- tests/test_api/test_api_arguments.py +25 -20
- tests/test_api/test_api_facts.py +28 -15
- tests/test_api/test_api_operations.py +43 -44
- tests/test_cli/test_cli.py +17 -17
- tests/test_cli/test_cli_inventory.py +4 -4
- tests/test_cli/test_context_objects.py +26 -26
- tests/test_connectors/test_docker.py +83 -43
- tests/test_connectors/test_ssh.py +153 -132
- tests/test_connectors/test_sshuserclient.py +10 -5
- {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/WHEEL +0 -0
- {pyinfra-3.3.dist-info → pyinfra-3.4.dist-info}/top_level.txt +0 -0
pyinfra/api/arguments.py
CHANGED
|
@@ -18,14 +18,12 @@ from typing import (
|
|
|
18
18
|
|
|
19
19
|
from typing_extensions import TypedDict
|
|
20
20
|
|
|
21
|
-
from pyinfra import context
|
|
22
21
|
from pyinfra.api.exceptions import ArgumentTypeError
|
|
23
|
-
from pyinfra.api.state import State
|
|
24
22
|
from pyinfra.api.util import raise_if_bad_type
|
|
23
|
+
from pyinfra.context import ctx_config
|
|
25
24
|
|
|
26
25
|
if TYPE_CHECKING:
|
|
27
|
-
from pyinfra.api
|
|
28
|
-
from pyinfra.api.host import Host
|
|
26
|
+
from pyinfra.api import Config, Host, State
|
|
29
27
|
|
|
30
28
|
T = TypeVar("T")
|
|
31
29
|
default_sentinel = object()
|
|
@@ -292,10 +290,9 @@ __argument_docs__ = {
|
|
|
292
290
|
|
|
293
291
|
|
|
294
292
|
def pop_global_arguments(
|
|
293
|
+
state: "State",
|
|
294
|
+
host: "Host",
|
|
295
295
|
kwargs: dict[str, Any],
|
|
296
|
-
state: Optional["State"] = None,
|
|
297
|
-
host: Optional["Host"] = None,
|
|
298
|
-
keys_to_check=None,
|
|
299
296
|
) -> tuple[AllArguments, list[str]]:
|
|
300
297
|
"""
|
|
301
298
|
Pop and return operation global keyword arguments, in preferred order:
|
|
@@ -306,22 +303,17 @@ def pop_global_arguments(
|
|
|
306
303
|
+ From the config variables
|
|
307
304
|
"""
|
|
308
305
|
|
|
309
|
-
state = state or context.state
|
|
310
|
-
host = host or context.host
|
|
311
|
-
|
|
312
306
|
config = state.config
|
|
313
|
-
if
|
|
314
|
-
config =
|
|
307
|
+
if ctx_config.isset():
|
|
308
|
+
config = config
|
|
315
309
|
|
|
316
|
-
|
|
310
|
+
cdkwargs = host.current_deploy_kwargs
|
|
311
|
+
meta_kwargs: dict[str, Any] = cdkwargs or {} # type: ignore[assignment]
|
|
317
312
|
|
|
318
313
|
arguments: dict[str, Any] = {}
|
|
319
314
|
found_keys: list[str] = []
|
|
320
315
|
|
|
321
316
|
for key, type_ in all_global_arguments():
|
|
322
|
-
if keys_to_check and key not in keys_to_check:
|
|
323
|
-
continue
|
|
324
|
-
|
|
325
317
|
argument_meta = all_argument_meta[key]
|
|
326
318
|
handler = argument_meta.handler
|
|
327
319
|
default: Any = argument_meta.default(config)
|
pyinfra/api/deploy.py
CHANGED
|
@@ -82,7 +82,7 @@ def deploy(
|
|
|
82
82
|
def _wrap_deploy(func: Callable[P, Any]) -> PyinfraOperation[P]:
|
|
83
83
|
@wraps(func)
|
|
84
84
|
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> Any:
|
|
85
|
-
deploy_kwargs, _ = pop_global_arguments(kwargs)
|
|
85
|
+
deploy_kwargs, _ = pop_global_arguments(context.state, context.host, kwargs)
|
|
86
86
|
|
|
87
87
|
deploy_data = getattr(func, "deploy_data", None)
|
|
88
88
|
|
pyinfra/api/facts.py
CHANGED
|
@@ -31,14 +31,12 @@ from pyinfra.api.util import (
|
|
|
31
31
|
print_host_combined_output,
|
|
32
32
|
)
|
|
33
33
|
from pyinfra.connectors.util import CommandOutput
|
|
34
|
-
from pyinfra.context import ctx_host, ctx_state
|
|
35
34
|
from pyinfra.progress import progress_spinner
|
|
36
35
|
|
|
37
36
|
from .arguments import CONNECTOR_ARGUMENT_KEYS
|
|
38
37
|
|
|
39
38
|
if TYPE_CHECKING:
|
|
40
|
-
from pyinfra.api
|
|
41
|
-
from pyinfra.api.state import State
|
|
39
|
+
from pyinfra.api import Host, State
|
|
42
40
|
|
|
43
41
|
SUDO_REGEX = r"^sudo: unknown user"
|
|
44
42
|
SU_REGEXES = (
|
|
@@ -121,21 +119,19 @@ def _make_command(command_attribute, host_args):
|
|
|
121
119
|
return command_attribute
|
|
122
120
|
|
|
123
121
|
|
|
124
|
-
def _handle_fact_kwargs(state, host, cls, args, kwargs):
|
|
122
|
+
def _handle_fact_kwargs(state: "State", host: "Host", cls, args, kwargs):
|
|
125
123
|
args = args or []
|
|
126
124
|
kwargs = kwargs or {}
|
|
127
125
|
|
|
128
126
|
# Start with a (shallow) copy of current operation kwargs if any
|
|
129
|
-
ctx_kwargs
|
|
127
|
+
ctx_kwargs: dict[str, Any] = (
|
|
128
|
+
cast(dict[str, Any], host.current_op_global_arguments) or {}
|
|
129
|
+
).copy()
|
|
130
130
|
# Update with the input kwargs (overrides)
|
|
131
131
|
ctx_kwargs.update(kwargs)
|
|
132
132
|
|
|
133
133
|
# Pop executor kwargs, pass remaining
|
|
134
|
-
global_kwargs, _ = pop_global_arguments(
|
|
135
|
-
ctx_kwargs,
|
|
136
|
-
state=state,
|
|
137
|
-
host=host,
|
|
138
|
-
)
|
|
134
|
+
global_kwargs, _ = pop_global_arguments(state, host, cast(dict[str, Any], ctx_kwargs))
|
|
139
135
|
|
|
140
136
|
fact_kwargs = {key: value for key, value in kwargs.items() if key not in global_kwargs}
|
|
141
137
|
|
|
@@ -146,14 +142,12 @@ def _handle_fact_kwargs(state, host, cls, args, kwargs):
|
|
|
146
142
|
return fact_kwargs, global_kwargs
|
|
147
143
|
|
|
148
144
|
|
|
149
|
-
def get_facts(state
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
with ctx_host.use(host):
|
|
153
|
-
return get_fact(state, host, *args, **kwargs)
|
|
145
|
+
def get_facts(state, *args, **kwargs):
|
|
146
|
+
def get_host_fact(host, *args, **kwargs):
|
|
147
|
+
return get_fact(state, host, *args, **kwargs)
|
|
154
148
|
|
|
155
149
|
greenlet_to_host = {
|
|
156
|
-
state.pool.spawn(
|
|
150
|
+
state.pool.spawn(get_host_fact, host, *args, **kwargs): host
|
|
157
151
|
for host in state.inventory.iter_active_hosts()
|
|
158
152
|
}
|
|
159
153
|
|
|
@@ -311,13 +305,3 @@ def _get_fact(
|
|
|
311
305
|
state.fail_hosts({host})
|
|
312
306
|
|
|
313
307
|
return data
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def get_host_fact(
|
|
317
|
-
state: "State",
|
|
318
|
-
host: "Host",
|
|
319
|
-
cls,
|
|
320
|
-
args: Optional[Iterable] = None,
|
|
321
|
-
kwargs: Optional[dict] = None,
|
|
322
|
-
) -> Any:
|
|
323
|
-
return get_fact(state, host, cls, args=args, kwargs=kwargs)
|
pyinfra/api/host.py
CHANGED
|
@@ -25,7 +25,7 @@ from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file
|
|
|
25
25
|
|
|
26
26
|
from .connectors import get_execution_connector
|
|
27
27
|
from .exceptions import ConnectError
|
|
28
|
-
from .facts import FactBase, ShortFactBase,
|
|
28
|
+
from .facts import FactBase, ShortFactBase, get_fact
|
|
29
29
|
from .util import memoize, sha1_hash
|
|
30
30
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
@@ -328,12 +328,18 @@ class Host:
|
|
|
328
328
|
|
|
329
329
|
return temp_directory
|
|
330
330
|
|
|
331
|
-
def get_temp_filename(
|
|
331
|
+
def get_temp_filename(
|
|
332
|
+
self,
|
|
333
|
+
hash_key: Optional[str] = None,
|
|
334
|
+
hash_filename: bool = True,
|
|
335
|
+
*,
|
|
336
|
+
temp_directory: Optional[str] = None,
|
|
337
|
+
):
|
|
332
338
|
"""
|
|
333
339
|
Generate a temporary filename for this deploy.
|
|
334
340
|
"""
|
|
335
341
|
|
|
336
|
-
temp_directory = self._get_temp_directory()
|
|
342
|
+
temp_directory = temp_directory or self._get_temp_directory()
|
|
337
343
|
|
|
338
344
|
if not hash_key:
|
|
339
345
|
hash_key = str(uuid4())
|
|
@@ -358,7 +364,7 @@ class Host:
|
|
|
358
364
|
"""
|
|
359
365
|
Get a fact for this host, reading from the cache if present.
|
|
360
366
|
"""
|
|
361
|
-
return
|
|
367
|
+
return get_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
|
|
362
368
|
|
|
363
369
|
# Connector proxy
|
|
364
370
|
#
|
pyinfra/api/inventory.py
CHANGED
|
@@ -36,6 +36,7 @@ class Inventory:
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
state: "State"
|
|
39
|
+
groups: dict[str, list[Host]]
|
|
39
40
|
|
|
40
41
|
@staticmethod
|
|
41
42
|
def empty():
|
|
@@ -181,7 +182,7 @@ class Inventory:
|
|
|
181
182
|
"""
|
|
182
183
|
return len(self.state.activated_hosts)
|
|
183
184
|
|
|
184
|
-
def get_host(self, name: str, default=NoHostError):
|
|
185
|
+
def get_host(self, name: str, default=NoHostError) -> Host:
|
|
185
186
|
"""
|
|
186
187
|
Get a single host by name.
|
|
187
188
|
"""
|
|
@@ -192,9 +193,10 @@ class Inventory:
|
|
|
192
193
|
if default is NoHostError:
|
|
193
194
|
raise NoHostError("No such host: {0}".format(name))
|
|
194
195
|
|
|
196
|
+
# TODO: remove default here?
|
|
195
197
|
return default
|
|
196
198
|
|
|
197
|
-
def get_group(self, name: str, default=NoGroupError):
|
|
199
|
+
def get_group(self, name: str, default=NoGroupError) -> list[Host]:
|
|
198
200
|
"""
|
|
199
201
|
Get a list of hosts belonging to a group.
|
|
200
202
|
"""
|
|
@@ -205,6 +207,7 @@ class Inventory:
|
|
|
205
207
|
if default is NoGroupError:
|
|
206
208
|
raise NoGroupError("No such group: {0}".format(name))
|
|
207
209
|
|
|
210
|
+
# TODO: remove default here?
|
|
208
211
|
return default
|
|
209
212
|
|
|
210
213
|
def get_data(self):
|
pyinfra/api/operation.py
CHANGED
|
@@ -232,7 +232,7 @@ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> Py
|
|
|
232
232
|
# Configure operation
|
|
233
233
|
#
|
|
234
234
|
# Get the meta kwargs (globals that apply to all hosts)
|
|
235
|
-
global_arguments, global_argument_keys = pop_global_arguments(kwargs)
|
|
235
|
+
global_arguments, global_argument_keys = pop_global_arguments(state, host, kwargs)
|
|
236
236
|
|
|
237
237
|
names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
|
|
238
238
|
op_order, op_hash = solve_operation_consistency(names, state, host)
|
pyinfra/api/util.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
from functools import wraps
|
|
4
|
-
from hashlib import sha1
|
|
5
|
+
from hashlib import md5, sha1, sha256
|
|
5
6
|
from inspect import getframeinfo, stack
|
|
6
7
|
from io import BytesIO, StringIO
|
|
7
8
|
from os import getcwd, path, stat
|
|
@@ -341,7 +342,7 @@ class get_file_io:
|
|
|
341
342
|
_close: bool = False
|
|
342
343
|
_file_io: IO[Any]
|
|
343
344
|
|
|
344
|
-
def __init__(self, filename_or_io, mode="rb"):
|
|
345
|
+
def __init__(self, filename_or_io: str | IO, mode: str = "rb"):
|
|
345
346
|
if not (
|
|
346
347
|
# Check we can be read
|
|
347
348
|
hasattr(filename_or_io, "read")
|
|
@@ -389,19 +390,32 @@ class get_file_io:
|
|
|
389
390
|
return self.filename_or_io
|
|
390
391
|
|
|
391
392
|
|
|
392
|
-
def
|
|
393
|
+
def get_file_md5(filename_or_io: str | IO):
|
|
394
|
+
return _get_file_digest(filename_or_io, md5())
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def get_file_sha1(filename_or_io: str | IO):
|
|
398
|
+
return _get_file_digest(filename_or_io, sha1())
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def get_file_sha256(filename_or_io: str | IO):
|
|
402
|
+
return _get_file_digest(filename_or_io, sha256())
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _get_file_digest(filename_or_io: str | IO, hasher: hashlib._Hash):
|
|
393
406
|
"""
|
|
394
|
-
Calculates the
|
|
407
|
+
Calculates the hash of a file or file object using a buffer to handle larger files.
|
|
395
408
|
"""
|
|
396
409
|
|
|
397
410
|
file_data = get_file_io(filename_or_io)
|
|
398
411
|
cache_key = file_data.cache_key
|
|
412
|
+
if cache_key:
|
|
413
|
+
cache_key = f"{cache_key}_{hasher.name}"
|
|
399
414
|
|
|
400
415
|
if cache_key and cache_key in FILE_SHAS:
|
|
401
416
|
return FILE_SHAS[cache_key]
|
|
402
417
|
|
|
403
418
|
with file_data as file_io:
|
|
404
|
-
hasher = sha1()
|
|
405
419
|
buff = file_io.read(BLOCKSIZE)
|
|
406
420
|
|
|
407
421
|
while len(buff) > 0:
|
|
@@ -425,7 +439,7 @@ def get_path_permissions_mode(pathname: str):
|
|
|
425
439
|
"""
|
|
426
440
|
|
|
427
441
|
mode_octal = oct(stat(pathname).st_mode)
|
|
428
|
-
return mode_octal[-3:]
|
|
442
|
+
return int(mode_octal[-3:])
|
|
429
443
|
|
|
430
444
|
|
|
431
445
|
def raise_if_bad_type(
|
pyinfra/connectors/docker.py
CHANGED
|
@@ -33,29 +33,6 @@ connector_data_meta: dict[str, DataMeta] = {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def _find_start_docker_container(container_id) -> tuple[str, bool]:
|
|
37
|
-
docker_info = local.shell("docker container inspect {0}".format(container_id))
|
|
38
|
-
assert isinstance(docker_info, str)
|
|
39
|
-
docker_info = json.loads(docker_info)[0]
|
|
40
|
-
if docker_info["State"]["Running"] is False:
|
|
41
|
-
logger.info("Starting stopped container: {0}".format(container_id))
|
|
42
|
-
local.shell("docker container start {0}".format(container_id))
|
|
43
|
-
return container_id, False
|
|
44
|
-
return container_id, True
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _start_docker_image(image_name):
|
|
48
|
-
try:
|
|
49
|
-
return local.shell(
|
|
50
|
-
"docker run -d {0} tail -f /dev/null".format(image_name),
|
|
51
|
-
splitlines=True,
|
|
52
|
-
)[
|
|
53
|
-
-1
|
|
54
|
-
] # last line is the container ID
|
|
55
|
-
except PyinfraError as e:
|
|
56
|
-
raise ConnectError(e.args[0])
|
|
57
|
-
|
|
58
|
-
|
|
59
36
|
class DockerConnector(BaseConnector):
|
|
60
37
|
"""
|
|
61
38
|
The Docker connector allows you to use pyinfra to create new Docker images or modify running
|
|
@@ -89,6 +66,9 @@ class DockerConnector(BaseConnector):
|
|
|
89
66
|
writing deploys, operations or facts.
|
|
90
67
|
"""
|
|
91
68
|
|
|
69
|
+
# enable the use of other docker cli compatible tools like podman
|
|
70
|
+
docker_cmd = "docker"
|
|
71
|
+
|
|
92
72
|
handles_execution = True
|
|
93
73
|
|
|
94
74
|
data_cls = ConnectorData
|
|
@@ -104,29 +84,54 @@ class DockerConnector(BaseConnector):
|
|
|
104
84
|
super().__init__(state, host)
|
|
105
85
|
self.local = LocalConnector(state, host)
|
|
106
86
|
|
|
87
|
+
@override
|
|
107
88
|
@staticmethod
|
|
108
89
|
def make_names_data(name=None):
|
|
109
90
|
if not name:
|
|
110
91
|
raise InventoryError("No docker base ID provided!")
|
|
111
92
|
|
|
112
93
|
yield (
|
|
113
|
-
"@docker/{
|
|
94
|
+
f"@docker/{name}",
|
|
114
95
|
{"docker_identifier": name},
|
|
115
96
|
["@docker"],
|
|
116
97
|
)
|
|
117
98
|
|
|
99
|
+
# 2 helper functions
|
|
100
|
+
def _find_start_docker_container(self, container_id) -> tuple[str, bool]:
|
|
101
|
+
docker_info = local.shell(f"{self.docker_cmd} container inspect {container_id}")
|
|
102
|
+
assert isinstance(docker_info, str)
|
|
103
|
+
docker_info = json.loads(docker_info)[0]
|
|
104
|
+
if docker_info["State"]["Running"] is False:
|
|
105
|
+
logger.info(f"Starting stopped container: {container_id}")
|
|
106
|
+
local.shell(f"{self.docker_cmd} container start {container_id}")
|
|
107
|
+
return container_id, False
|
|
108
|
+
return container_id, True
|
|
109
|
+
|
|
110
|
+
def _start_docker_image(self, image_name):
|
|
111
|
+
try:
|
|
112
|
+
return local.shell(
|
|
113
|
+
f"{self.docker_cmd} run -d {image_name} tail -f /dev/null",
|
|
114
|
+
splitlines=True,
|
|
115
|
+
)[
|
|
116
|
+
-1
|
|
117
|
+
] # last line is the container ID
|
|
118
|
+
except PyinfraError as e:
|
|
119
|
+
raise ConnectError(e.args[0])
|
|
120
|
+
|
|
118
121
|
@override
|
|
119
122
|
def connect(self) -> None:
|
|
120
123
|
self.local.connect()
|
|
121
124
|
|
|
122
125
|
docker_identifier = self.data["docker_identifier"]
|
|
123
|
-
with progress_spinner({"prepare
|
|
126
|
+
with progress_spinner({f"prepare {self.docker_cmd} container"}):
|
|
124
127
|
try:
|
|
125
|
-
self.container_id, was_running = _find_start_docker_container(
|
|
128
|
+
self.container_id, was_running = self._find_start_docker_container(
|
|
129
|
+
docker_identifier
|
|
130
|
+
)
|
|
126
131
|
if was_running:
|
|
127
132
|
self.no_stop = True
|
|
128
133
|
except PyinfraError:
|
|
129
|
-
self.container_id = _start_docker_image(docker_identifier)
|
|
134
|
+
self.container_id = self._start_docker_image(docker_identifier)
|
|
130
135
|
|
|
131
136
|
@override
|
|
132
137
|
def disconnect(self) -> None:
|
|
@@ -134,26 +139,28 @@ class DockerConnector(BaseConnector):
|
|
|
134
139
|
|
|
135
140
|
if self.no_stop:
|
|
136
141
|
logger.info(
|
|
137
|
-
"{0}
|
|
142
|
+
"{0}{1} build complete, container left running: {2}".format(
|
|
138
143
|
self.host.print_prefix,
|
|
144
|
+
self.docker_cmd,
|
|
139
145
|
click.style(container_id, bold=True),
|
|
140
146
|
),
|
|
141
147
|
)
|
|
142
148
|
return
|
|
143
149
|
|
|
144
|
-
with progress_spinner({"
|
|
145
|
-
image_id = local.shell("
|
|
150
|
+
with progress_spinner({f"{self.docker_cmd} commit"}):
|
|
151
|
+
image_id = local.shell(f"{self.docker_cmd} commit {container_id}", splitlines=True)[-1][
|
|
146
152
|
7:19
|
|
147
153
|
] # last line is the image ID, get sha256:[XXXXXXXXXX]...
|
|
148
154
|
|
|
149
|
-
with progress_spinner({"
|
|
155
|
+
with progress_spinner({f"{self.docker_cmd} rm"}):
|
|
150
156
|
local.shell(
|
|
151
|
-
"
|
|
157
|
+
f"{self.docker_cmd} rm -f {container_id}",
|
|
152
158
|
)
|
|
153
159
|
|
|
154
160
|
logger.info(
|
|
155
|
-
"{0}
|
|
161
|
+
"{0}{1} build complete, image ID: {2}".format(
|
|
156
162
|
self.host.print_prefix,
|
|
163
|
+
self.docker_cmd,
|
|
157
164
|
click.style(image_id, bold=True),
|
|
158
165
|
),
|
|
159
166
|
)
|
|
@@ -175,7 +182,7 @@ class DockerConnector(BaseConnector):
|
|
|
175
182
|
|
|
176
183
|
docker_flags = "-it" if local_arguments.get("_get_pty") else "-i"
|
|
177
184
|
docker_command = StringCommand(
|
|
178
|
-
|
|
185
|
+
self.docker_cmd,
|
|
179
186
|
"exec",
|
|
180
187
|
docker_flags,
|
|
181
188
|
container_id,
|
|
@@ -202,7 +209,7 @@ class DockerConnector(BaseConnector):
|
|
|
202
209
|
**kwargs, # ignored (sudo/etc)
|
|
203
210
|
) -> bool:
|
|
204
211
|
"""
|
|
205
|
-
Upload a file/IO object to the target
|
|
212
|
+
Upload a file/IO object to the target container by copying it to a
|
|
206
213
|
temporary location and then uploading it into the container using ``docker cp``.
|
|
207
214
|
"""
|
|
208
215
|
|
|
@@ -220,7 +227,7 @@ class DockerConnector(BaseConnector):
|
|
|
220
227
|
temp_f.write(data)
|
|
221
228
|
|
|
222
229
|
docker_command = StringCommand(
|
|
223
|
-
|
|
230
|
+
self.docker_cmd,
|
|
224
231
|
"cp",
|
|
225
232
|
temp_filename,
|
|
226
233
|
f"{self.container_id}:{remote_filename}",
|
|
@@ -260,7 +267,7 @@ class DockerConnector(BaseConnector):
|
|
|
260
267
|
**kwargs, # ignored (sudo/etc)
|
|
261
268
|
) -> bool:
|
|
262
269
|
"""
|
|
263
|
-
Download a file from the target
|
|
270
|
+
Download a file from the target container by copying it to a temporary
|
|
264
271
|
location and then reading that into our final file/IO object.
|
|
265
272
|
"""
|
|
266
273
|
|
|
@@ -268,7 +275,7 @@ class DockerConnector(BaseConnector):
|
|
|
268
275
|
|
|
269
276
|
try:
|
|
270
277
|
docker_command = StringCommand(
|
|
271
|
-
|
|
278
|
+
self.docker_cmd,
|
|
272
279
|
"cp",
|
|
273
280
|
f"{self.container_id}:{remote_filename}",
|
|
274
281
|
temp_filename,
|
|
@@ -302,3 +309,75 @@ class DockerConnector(BaseConnector):
|
|
|
302
309
|
)
|
|
303
310
|
|
|
304
311
|
return status
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class PodmanConnector(DockerConnector):
|
|
315
|
+
"""
|
|
316
|
+
The Podman connector allows you to use pyinfra to create new Podman images or modify running
|
|
317
|
+
Podman containers.
|
|
318
|
+
|
|
319
|
+
.. note::
|
|
320
|
+
|
|
321
|
+
The Podman connector allows pyinfra to target Podman containers as inventory and is
|
|
322
|
+
unrelated to the :doc:`../operations/docker` & :doc:`../facts/docker`.
|
|
323
|
+
|
|
324
|
+
You can pass either an image name or existing container ID:
|
|
325
|
+
|
|
326
|
+
+ Image - will create a new container from the image, execute operations against it, save into \
|
|
327
|
+
a new Podman image and remove the container
|
|
328
|
+
+ Existing container ID - will execute operations against the running container, leaving it \
|
|
329
|
+
running
|
|
330
|
+
|
|
331
|
+
.. code:: shell
|
|
332
|
+
|
|
333
|
+
# A Podman base image must be provided
|
|
334
|
+
pyinfra @podman/alpine:3.8 ...
|
|
335
|
+
|
|
336
|
+
# pyinfra can run on multiple Docker images in parallel
|
|
337
|
+
pyinfra @podman/alpine:3.8,@podman/ubuntu:bionic ...
|
|
338
|
+
|
|
339
|
+
# Execute against a running container
|
|
340
|
+
pyinfra @podman/2beb8c15a1b1 ...
|
|
341
|
+
|
|
342
|
+
The Podman connector is great for testing pyinfra operations locally, rather than connecting to
|
|
343
|
+
a remote host over SSH each time. This gives you a fast, local-first devloop to iterate on when
|
|
344
|
+
writing deploys, operations or facts.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
docker_cmd = "podman"
|
|
348
|
+
|
|
349
|
+
@override
|
|
350
|
+
@staticmethod
|
|
351
|
+
def make_names_data(name=None):
|
|
352
|
+
if not name:
|
|
353
|
+
raise InventoryError("No podman base ID provided!")
|
|
354
|
+
|
|
355
|
+
yield (
|
|
356
|
+
f"@podman/{name}",
|
|
357
|
+
{"docker_identifier": name},
|
|
358
|
+
["@podman"],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Duplicate function definition to swap the docstring.
|
|
362
|
+
@override
|
|
363
|
+
def put_file(
|
|
364
|
+
self,
|
|
365
|
+
filename_or_io,
|
|
366
|
+
remote_filename,
|
|
367
|
+
remote_temp_filename=None, # ignored
|
|
368
|
+
print_output=False,
|
|
369
|
+
print_input=False,
|
|
370
|
+
**kwargs, # ignored (sudo/etc)
|
|
371
|
+
) -> bool:
|
|
372
|
+
"""
|
|
373
|
+
Upload a file/IO object to the target container by copying it to a
|
|
374
|
+
temporary location and then uploading it into the container using ``podman cp``.
|
|
375
|
+
"""
|
|
376
|
+
return super().put_file(
|
|
377
|
+
filename_or_io,
|
|
378
|
+
remote_filename,
|
|
379
|
+
remote_temp_filename, # ignored
|
|
380
|
+
print_output,
|
|
381
|
+
print_input,
|
|
382
|
+
**kwargs, # ignored (sudo/etc)
|
|
383
|
+
)
|
pyinfra/connectors/dockerssh.py
CHANGED
pyinfra/connectors/local.py
CHANGED
pyinfra/connectors/ssh.py
CHANGED
|
@@ -158,6 +158,7 @@ class SSHClient(ParamikoClient):
|
|
|
158
158
|
forward_agent,
|
|
159
159
|
missing_host_key_policy,
|
|
160
160
|
host_keys_file,
|
|
161
|
+
keep_alive,
|
|
161
162
|
) = self.parse_config(
|
|
162
163
|
hostname,
|
|
163
164
|
kwargs,
|
|
@@ -183,8 +184,6 @@ class SSHClient(ParamikoClient):
|
|
|
183
184
|
if _pyinfra_ssh_forward_agent is not None:
|
|
184
185
|
forward_agent = _pyinfra_ssh_forward_agent
|
|
185
186
|
|
|
186
|
-
keep_alive = config.get("keep_alive")
|
|
187
|
-
|
|
188
187
|
if keep_alive:
|
|
189
188
|
transport = self.get_transport()
|
|
190
189
|
assert transport is not None, "No transport"
|
|
@@ -215,13 +214,14 @@ class SSHClient(ParamikoClient):
|
|
|
215
214
|
cfg: dict = {"port": 22}
|
|
216
215
|
cfg.update(initial_cfg or {})
|
|
217
216
|
|
|
217
|
+
keep_alive = 0
|
|
218
218
|
forward_agent = False
|
|
219
219
|
missing_host_key_policy = get_missing_host_key_policy(strict_host_key_checking)
|
|
220
220
|
host_keys_file = path.expanduser("~/.ssh/known_hosts") # OpenSSH default
|
|
221
221
|
|
|
222
222
|
ssh_config = get_ssh_config(ssh_config_file)
|
|
223
223
|
if not ssh_config:
|
|
224
|
-
return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file
|
|
224
|
+
return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
|
|
225
225
|
|
|
226
226
|
host_config = ssh_config.lookup(hostname)
|
|
227
227
|
forward_agent = host_config.get("forwardagent") == "yes"
|
|
@@ -248,7 +248,7 @@ class SSHClient(ParamikoClient):
|
|
|
248
248
|
cfg["port"] = int(host_config["port"])
|
|
249
249
|
|
|
250
250
|
if "serveraliveinterval" in host_config:
|
|
251
|
-
|
|
251
|
+
keep_alive = int(host_config["serveraliveinterval"])
|
|
252
252
|
|
|
253
253
|
if "proxycommand" in host_config:
|
|
254
254
|
cfg["sock"] = ProxyCommand(host_config["proxycommand"])
|
|
@@ -275,7 +275,7 @@ class SSHClient(ParamikoClient):
|
|
|
275
275
|
sock = c.gateway(hostname, cfg["port"], target, target_config["port"])
|
|
276
276
|
cfg["sock"] = sock
|
|
277
277
|
|
|
278
|
-
return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file
|
|
278
|
+
return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
|
|
279
279
|
|
|
280
280
|
@staticmethod
|
|
281
281
|
def derive_shorthand(ssh_config, host_string):
|
pyinfra/connectors/terraform.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
3
5
|
from pyinfra import local, logger
|
|
4
6
|
from pyinfra.api.exceptions import InventoryError
|
|
5
7
|
from pyinfra.api.util import memoize
|
|
@@ -76,6 +78,7 @@ class TerraformInventoryConnector(BaseConnector):
|
|
|
76
78
|
|
|
77
79
|
"""
|
|
78
80
|
|
|
81
|
+
@override
|
|
79
82
|
@staticmethod
|
|
80
83
|
def make_names_data(name=None):
|
|
81
84
|
show_warning()
|
pyinfra/connectors/vagrant.py
CHANGED
|
@@ -3,6 +3,8 @@ from os import path
|
|
|
3
3
|
from queue import Queue
|
|
4
4
|
from threading import Thread
|
|
5
5
|
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
|
|
6
8
|
from pyinfra import local, logger
|
|
7
9
|
from pyinfra.api.exceptions import InventoryError
|
|
8
10
|
from pyinfra.api.util import memoize
|
|
@@ -131,6 +133,7 @@ class VagrantInventoryConnector(BaseConnector):
|
|
|
131
133
|
pyinfra @vagrant/my-vm-name,@vagrant/another-vm-name ...
|
|
132
134
|
"""
|
|
133
135
|
|
|
136
|
+
@override
|
|
134
137
|
@staticmethod
|
|
135
138
|
def make_names_data(name=None):
|
|
136
139
|
vagrant_ssh_info = get_vagrant_config(name)
|