pyinfra 3.5.1__py3-none-any.whl → 3.6.1__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/__init__.py +1 -0
- pyinfra/api/arguments.py +9 -2
- pyinfra/api/arguments_typed.py +18 -23
- pyinfra/api/command.py +9 -3
- pyinfra/api/deploy.py +1 -1
- pyinfra/api/exceptions.py +12 -0
- pyinfra/api/facts.py +20 -4
- pyinfra/api/host.py +3 -0
- pyinfra/api/inventory.py +2 -2
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operation.py +9 -4
- pyinfra/api/operations.py +16 -14
- pyinfra/api/util.py +22 -5
- pyinfra/connectors/docker.py +25 -1
- pyinfra/connectors/ssh.py +57 -0
- pyinfra/connectors/sshuserclient/client.py +47 -28
- pyinfra/connectors/util.py +16 -9
- pyinfra/facts/crontab.py +10 -8
- pyinfra/facts/files.py +12 -3
- pyinfra/facts/flatpak.py +1 -1
- pyinfra/facts/npm.py +1 -1
- pyinfra/facts/server.py +18 -2
- pyinfra/operations/apk.py +2 -1
- pyinfra/operations/apt.py +15 -7
- pyinfra/operations/brew.py +1 -0
- pyinfra/operations/crontab.py +4 -1
- pyinfra/operations/dnf.py +4 -1
- pyinfra/operations/docker.py +70 -16
- pyinfra/operations/files.py +87 -12
- pyinfra/operations/flatpak.py +1 -0
- pyinfra/operations/gem.py +1 -0
- pyinfra/operations/git.py +1 -0
- pyinfra/operations/iptables.py +1 -0
- pyinfra/operations/lxd.py +1 -0
- pyinfra/operations/mysql.py +1 -0
- pyinfra/operations/opkg.py +2 -1
- pyinfra/operations/pacman.py +1 -0
- pyinfra/operations/pip.py +1 -0
- pyinfra/operations/pipx.py +1 -0
- pyinfra/operations/pkg.py +1 -0
- pyinfra/operations/pkgin.py +1 -0
- pyinfra/operations/postgres.py +7 -1
- pyinfra/operations/puppet.py +1 -0
- pyinfra/operations/python.py +1 -0
- pyinfra/operations/selinux.py +1 -0
- pyinfra/operations/server.py +1 -0
- pyinfra/operations/snap.py +2 -1
- pyinfra/operations/ssh.py +1 -0
- pyinfra/operations/systemd.py +1 -0
- pyinfra/operations/sysvinit.py +2 -1
- pyinfra/operations/util/docker.py +172 -8
- pyinfra/operations/util/packaging.py +2 -0
- pyinfra/operations/xbps.py +1 -0
- pyinfra/operations/yum.py +4 -1
- pyinfra/operations/zfs.py +1 -0
- pyinfra/operations/zypper.py +1 -0
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.1.dist-info}/METADATA +2 -1
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.1.dist-info}/RECORD +64 -63
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.1.dist-info}/WHEEL +1 -1
- pyinfra_cli/cli.py +20 -4
- pyinfra_cli/inventory.py +26 -1
- pyinfra_cli/util.py +1 -1
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.1.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -28,15 +28,17 @@ HOST_KEYS_LOCK = BoundedSemaphore()
|
|
|
28
28
|
class StrictPolicy(MissingHostKeyPolicy):
|
|
29
29
|
@override
|
|
30
30
|
def missing_host_key(self, client, hostname, key):
|
|
31
|
-
logger.error("No host key for
|
|
32
|
-
raise SSHException(
|
|
33
|
-
"StrictPolicy: No host key for {0} found in known_hosts".format(hostname),
|
|
34
|
-
)
|
|
31
|
+
logger.error("No host key for %s found in known_hosts", hostname)
|
|
32
|
+
raise SSHException(f"StrictPolicy: No host key for {hostname} found in known_hosts")
|
|
35
33
|
|
|
36
34
|
|
|
37
35
|
def append_hostkey(client, hostname, key):
|
|
38
36
|
"""Append hostname to the clients host_keys_file"""
|
|
39
37
|
|
|
38
|
+
if client._host_keys_filename is None:
|
|
39
|
+
logger.warning("No host keys filename, not saving key for: %s", hostname)
|
|
40
|
+
return
|
|
41
|
+
|
|
40
42
|
with HOST_KEYS_LOCK:
|
|
41
43
|
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
42
44
|
# this correctly, so use that with the client filename variable.
|
|
@@ -67,30 +69,28 @@ class AcceptNewPolicy(MissingHostKeyPolicy):
|
|
|
67
69
|
)
|
|
68
70
|
|
|
69
71
|
append_hostkey(client, hostname, key)
|
|
70
|
-
logger.warning("Added host key for
|
|
72
|
+
logger.warning("Added host key for %s to known_hosts", hostname)
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
class AskPolicy(MissingHostKeyPolicy):
|
|
74
76
|
@override
|
|
75
77
|
def missing_host_key(self, client, hostname, key):
|
|
76
78
|
should_continue = input(
|
|
77
|
-
"No host key for {
|
|
78
|
-
hostname,
|
|
79
|
-
),
|
|
79
|
+
f"No host key for {hostname} found in known_hosts, do you want to continue [y/n] ",
|
|
80
80
|
)
|
|
81
81
|
if should_continue.lower() != "y":
|
|
82
82
|
raise SSHException(
|
|
83
|
-
"AskPolicy: No host key for {
|
|
83
|
+
f"AskPolicy: No host key for {hostname} found in known_hosts",
|
|
84
84
|
)
|
|
85
85
|
append_hostkey(client, hostname, key)
|
|
86
|
-
logger.warning("Added host key for
|
|
86
|
+
logger.warning("Added host key for %s to known_hosts", hostname)
|
|
87
87
|
return
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
class WarningPolicy(MissingHostKeyPolicy):
|
|
91
91
|
@override
|
|
92
92
|
def missing_host_key(self, client, hostname, key):
|
|
93
|
-
logger.warning("No host key for
|
|
93
|
+
logger.warning("No host key for %s found in known_hosts", hostname)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def get_missing_host_key_policy(policy):
|
|
@@ -102,7 +102,7 @@ def get_missing_host_key_policy(policy):
|
|
|
102
102
|
return StrictPolicy()
|
|
103
103
|
if policy == "accept-new":
|
|
104
104
|
return AcceptNewPolicy()
|
|
105
|
-
raise SSHException("Invalid value StrictHostKeyChecking={}"
|
|
105
|
+
raise SSHException(f"Invalid value StrictHostKeyChecking={policy}")
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
@memoize
|
|
@@ -120,17 +120,24 @@ def get_ssh_config(user_config_file=None):
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
@memoize
|
|
123
|
-
def get_host_keys(
|
|
123
|
+
def get_host_keys(filenames):
|
|
124
|
+
"""
|
|
125
|
+
Load host keys from one or more files.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
filenames: A tuple of filenames to load host keys from.
|
|
129
|
+
"""
|
|
124
130
|
with HOST_KEYS_LOCK:
|
|
125
131
|
host_keys = HostKeys()
|
|
126
132
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
for filename in filenames:
|
|
134
|
+
try:
|
|
135
|
+
host_keys.load(filename)
|
|
136
|
+
# When paramiko encounters a bad host keys line it sometimes bails the
|
|
137
|
+
# entire load incorrectly.
|
|
138
|
+
# See: https://github.com/paramiko/paramiko/pull/1990
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning("Failed to load host keys from %s: %s", filename, e)
|
|
134
141
|
|
|
135
142
|
return host_keys
|
|
136
143
|
|
|
@@ -157,7 +164,7 @@ class SSHClient(ParamikoClient):
|
|
|
157
164
|
config,
|
|
158
165
|
forward_agent,
|
|
159
166
|
missing_host_key_policy,
|
|
160
|
-
|
|
167
|
+
host_keys_files,
|
|
161
168
|
keep_alive,
|
|
162
169
|
) = self.parse_config(
|
|
163
170
|
hostname,
|
|
@@ -169,11 +176,13 @@ class SSHClient(ParamikoClient):
|
|
|
169
176
|
config.update(kwargs)
|
|
170
177
|
|
|
171
178
|
if _pyinfra_ssh_known_hosts_file:
|
|
172
|
-
|
|
179
|
+
host_keys_files = (path.expanduser(_pyinfra_ssh_known_hosts_file),)
|
|
173
180
|
|
|
174
181
|
# Overwrite paramiko empty defaults with @memoize-d host keys object
|
|
175
|
-
self._host_keys = get_host_keys(
|
|
176
|
-
|
|
182
|
+
self._host_keys = get_host_keys(host_keys_files)
|
|
183
|
+
# Use the first file for writing new host keys
|
|
184
|
+
if len(host_keys_files) > 0:
|
|
185
|
+
self._host_keys_filename = host_keys_files[0]
|
|
177
186
|
|
|
178
187
|
if _pyinfra_ssh_paramiko_connect_kwargs:
|
|
179
188
|
config.update(_pyinfra_ssh_paramiko_connect_kwargs)
|
|
@@ -217,11 +226,18 @@ class SSHClient(ParamikoClient):
|
|
|
217
226
|
keep_alive = 0
|
|
218
227
|
forward_agent = False
|
|
219
228
|
missing_host_key_policy = get_missing_host_key_policy(strict_host_key_checking)
|
|
220
|
-
|
|
229
|
+
host_keys_files = (path.expanduser("~/.ssh/known_hosts"),)
|
|
221
230
|
|
|
222
231
|
ssh_config = get_ssh_config(ssh_config_file)
|
|
223
232
|
if not ssh_config:
|
|
224
|
-
return
|
|
233
|
+
return (
|
|
234
|
+
hostname,
|
|
235
|
+
cfg,
|
|
236
|
+
forward_agent,
|
|
237
|
+
missing_host_key_policy,
|
|
238
|
+
host_keys_files,
|
|
239
|
+
keep_alive,
|
|
240
|
+
)
|
|
225
241
|
|
|
226
242
|
host_config = ssh_config.lookup(hostname)
|
|
227
243
|
forward_agent = host_config.get("forwardagent") == "yes"
|
|
@@ -233,7 +249,10 @@ class SSHClient(ParamikoClient):
|
|
|
233
249
|
)
|
|
234
250
|
|
|
235
251
|
if "userknownhostsfile" in host_config:
|
|
236
|
-
|
|
252
|
+
# OpenSSH supports multiple space-separated known hosts files
|
|
253
|
+
host_keys_files = tuple(
|
|
254
|
+
path.expanduser(f) for f in host_config["userknownhostsfile"].split()
|
|
255
|
+
)
|
|
237
256
|
|
|
238
257
|
if "hostname" in host_config:
|
|
239
258
|
hostname = host_config["hostname"]
|
|
@@ -275,7 +294,7 @@ class SSHClient(ParamikoClient):
|
|
|
275
294
|
sock = c.gateway(hostname, cfg["port"], target, target_config["port"])
|
|
276
295
|
cfg["sock"] = sock
|
|
277
296
|
|
|
278
|
-
return hostname, cfg, forward_agent, missing_host_key_policy,
|
|
297
|
+
return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_files, keep_alive
|
|
279
298
|
|
|
280
299
|
@staticmethod
|
|
281
300
|
def derive_shorthand(ssh_config, host_string):
|
pyinfra/connectors/util.py
CHANGED
|
@@ -22,17 +22,17 @@ if TYPE_CHECKING:
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
SUDO_ASKPASS_ENV_VAR = "PYINFRA_SUDO_PASSWORD"
|
|
25
|
+
|
|
26
|
+
|
|
25
27
|
SUDO_ASKPASS_COMMAND = r"""
|
|
26
|
-
temp=$(mktemp "${{TMPDIR
|
|
28
|
+
temp=$(mktemp "${{TMPDIR:={0}}}/pyinfra-sudo-askpass-XXXXXXXXXXXX")
|
|
27
29
|
cat >"$temp"<<'__EOF__'
|
|
28
30
|
#!/bin/sh
|
|
29
|
-
printf '%s\n' "${
|
|
31
|
+
printf '%s\n' "${1}"
|
|
30
32
|
__EOF__
|
|
31
33
|
chmod 755 "$temp"
|
|
32
34
|
echo "$temp"
|
|
33
|
-
"""
|
|
34
|
-
SUDO_ASKPASS_ENV_VAR,
|
|
35
|
-
)
|
|
35
|
+
"""
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def run_local_process(
|
|
@@ -44,11 +44,14 @@ def run_local_process(
|
|
|
44
44
|
) -> tuple[int, "CommandOutput"]:
|
|
45
45
|
process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
|
46
46
|
|
|
47
|
-
if stdin:
|
|
48
|
-
write_stdin(stdin, process.stdin)
|
|
49
|
-
|
|
50
47
|
assert process.stdout is not None
|
|
51
48
|
assert process.stderr is not None
|
|
49
|
+
assert process.stdin is not None
|
|
50
|
+
|
|
51
|
+
# Write any stdin and then close it
|
|
52
|
+
if stdin:
|
|
53
|
+
write_stdin(stdin, process.stdin)
|
|
54
|
+
process.stdin.close()
|
|
52
55
|
|
|
53
56
|
combined_output = read_output_buffers(
|
|
54
57
|
process.stdout,
|
|
@@ -264,7 +267,9 @@ def extract_control_arguments(arguments: "ConnectorArguments") -> "ConnectorArgu
|
|
|
264
267
|
def _ensure_sudo_askpass_set_for_host(host: "Host"):
|
|
265
268
|
if host.connector_data.get("sudo_askpass_path"):
|
|
266
269
|
return
|
|
267
|
-
_, output = host.run_shell_command(
|
|
270
|
+
_, output = host.run_shell_command(
|
|
271
|
+
SUDO_ASKPASS_COMMAND.format(host.get_temp_dir_config(), SUDO_ASKPASS_ENV_VAR)
|
|
272
|
+
)
|
|
268
273
|
host.connector_data["sudo_askpass_path"] = shlex.quote(output.stdout_lines[0])
|
|
269
274
|
|
|
270
275
|
|
|
@@ -318,6 +323,8 @@ def make_unix_command(
|
|
|
318
323
|
_retries=0,
|
|
319
324
|
_retry_delay=0,
|
|
320
325
|
_retry_until=None,
|
|
326
|
+
# Temp dir config (ignored in command generation, used for temp file path generation)
|
|
327
|
+
_temp_dir=None,
|
|
321
328
|
) -> StringCommand:
|
|
322
329
|
"""
|
|
323
330
|
Builds a shell command with various kwargs.
|
pyinfra/facts/crontab.py
CHANGED
|
@@ -52,9 +52,11 @@ class CrontabFile:
|
|
|
52
52
|
|
|
53
53
|
name_comment = "# pyinfra-name={0}".format(name)
|
|
54
54
|
for cmd in self.commands:
|
|
55
|
+
if "command" not in cmd:
|
|
56
|
+
continue
|
|
55
57
|
if cmd.get("command") == command:
|
|
56
58
|
return cmd
|
|
57
|
-
if cmd.get("comments"
|
|
59
|
+
if name_comment in cmd.get("comments", []):
|
|
58
60
|
return cmd
|
|
59
61
|
return None
|
|
60
62
|
|
|
@@ -125,15 +127,15 @@ class Crontab(FactBase[CrontabFile]):
|
|
|
125
127
|
# or CrontabFile.to_json()
|
|
126
128
|
[
|
|
127
129
|
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
"command": "/path/to/command",
|
|
131
|
+
"minute": "*",
|
|
132
|
+
"hour": "*",
|
|
133
|
+
"month": "*",
|
|
134
|
+
"day_of_month": "*",
|
|
135
|
+
"day_of_week": "*",
|
|
134
136
|
},
|
|
135
137
|
{
|
|
136
|
-
"command": "echo another command
|
|
138
|
+
"command": "echo another command",
|
|
137
139
|
"special_time": "@daily",
|
|
138
140
|
}
|
|
139
141
|
]
|
pyinfra/facts/files.py
CHANGED
|
@@ -547,6 +547,8 @@ class FindFilesBase(FactBase):
|
|
|
547
547
|
command.append("-regex")
|
|
548
548
|
command.append(maybe_quote(regex))
|
|
549
549
|
|
|
550
|
+
command.extend(args)
|
|
551
|
+
|
|
550
552
|
command.append("||")
|
|
551
553
|
command.append("true")
|
|
552
554
|
|
|
@@ -664,14 +666,21 @@ class Block(FactBase):
|
|
|
664
666
|
|
|
665
667
|
class FileContents(FactBase):
|
|
666
668
|
"""
|
|
667
|
-
Returns the contents of a file as a list of lines
|
|
668
|
-
``None`` if the file doest not exist.
|
|
669
|
+
Returns the contents of a file as a list of lines, or ``None`` if the file does not exist.
|
|
669
670
|
"""
|
|
670
671
|
|
|
671
672
|
@override
|
|
672
673
|
def command(self, path):
|
|
673
|
-
|
|
674
|
+
self.missing_flag = "{0}{1}".format(MISSING, path)
|
|
675
|
+
return make_formatted_string_command(
|
|
676
|
+
"( test -e {0} && cat {0} ) || echo {1}",
|
|
677
|
+
QuoteString(path),
|
|
678
|
+
QuoteString(self.missing_flag),
|
|
679
|
+
)
|
|
674
680
|
|
|
675
681
|
@override
|
|
676
682
|
def process(self, output):
|
|
683
|
+
# If output is the missing flag, the file doesn't exist
|
|
684
|
+
if output and output[0] == self.missing_flag:
|
|
685
|
+
return None
|
|
677
686
|
return output
|
pyinfra/facts/flatpak.py
CHANGED
pyinfra/facts/npm.py
CHANGED
|
@@ -30,7 +30,7 @@ class NpmPackages(FactBase):
|
|
|
30
30
|
@override
|
|
31
31
|
def command(self, directory=None):
|
|
32
32
|
if directory:
|
|
33
|
-
return ("cd {0} && npm list -g --depth=0").format(directory)
|
|
33
|
+
return ("! test -d {0} || (cd {0} && npm list -g --depth=0)").format(directory)
|
|
34
34
|
return "npm list -g --depth=0"
|
|
35
35
|
|
|
36
36
|
@override
|
pyinfra/facts/server.py
CHANGED
|
@@ -52,12 +52,28 @@ class Path(FactBase):
|
|
|
52
52
|
|
|
53
53
|
class TmpDir(FactBase):
|
|
54
54
|
"""
|
|
55
|
-
Returns the temporary directory of the current server
|
|
55
|
+
Returns the temporary directory of the current server.
|
|
56
|
+
|
|
57
|
+
According to POSIX standards, checks environment variables in this order:
|
|
58
|
+
1. TMPDIR (if set and accessible)
|
|
59
|
+
2. TMP (if set and accessible)
|
|
60
|
+
3. TEMP (if set and accessible)
|
|
61
|
+
4. Falls back to empty string
|
|
56
62
|
"""
|
|
57
63
|
|
|
58
64
|
@override
|
|
59
65
|
def command(self):
|
|
60
|
-
return "
|
|
66
|
+
return """
|
|
67
|
+
if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ] && [ -w "$TMPDIR" ]; then
|
|
68
|
+
echo "$TMPDIR"
|
|
69
|
+
elif [ -n "$TMP" ] && [ -d "$TMP" ] && [ -w "$TMP" ]; then
|
|
70
|
+
echo "$TMP"
|
|
71
|
+
elif [ -n "$TEMP" ] && [ -d "$TEMP" ] && [ -w "$TEMP" ]; then
|
|
72
|
+
echo "$TEMP"
|
|
73
|
+
else
|
|
74
|
+
echo ""
|
|
75
|
+
fi
|
|
76
|
+
""".strip()
|
|
61
77
|
|
|
62
78
|
|
|
63
79
|
class Hostname(FactBase):
|
pyinfra/operations/apk.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Manage apk packages.
|
|
2
|
+
Manage apk packages. (Alpine Linux)
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -64,6 +64,7 @@ def packages(
|
|
|
64
64
|
|
|
65
65
|
.. code:: python
|
|
66
66
|
|
|
67
|
+
from pyinfra.operations import apk
|
|
67
68
|
# Update package list and install packages
|
|
68
69
|
apk.packages(
|
|
69
70
|
name="Install Asterisk and Vim",
|
pyinfra/operations/apt.py
CHANGED
|
@@ -4,7 +4,7 @@ Manage apt packages and repositories.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from datetime import timedelta
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from urllib.parse import urlparse
|
|
9
9
|
|
|
10
10
|
from pyinfra import host
|
|
@@ -67,6 +67,7 @@ def key(src: str | None = None, keyserver: str | None = None, keyid: str | list[
|
|
|
67
67
|
|
|
68
68
|
.. code:: python
|
|
69
69
|
|
|
70
|
+
from pyinfra.operations import apt
|
|
70
71
|
# Note: If using URL, wget is assumed to be installed.
|
|
71
72
|
apt.key(
|
|
72
73
|
name="Add the Docker apt gpg key",
|
|
@@ -309,12 +310,19 @@ def update(cache_time: int | None = None):
|
|
|
309
310
|
# Ubuntu provides this handy file
|
|
310
311
|
cache_info = host.get_fact(File, path=APT_UPDATE_FILENAME)
|
|
311
312
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
if cache_info and cache_info["mtime"]:
|
|
314
|
+
# The fact Date contains the date of the server in its timezone.
|
|
315
|
+
# cache_info["mtime"] ignores the timezone and consider the timestamp as UTC.
|
|
316
|
+
# So let's do the same here for the server current Date : ignore the
|
|
317
|
+
# timezone and consider it as UTC to have correct comparison with
|
|
318
|
+
# cache_info["mtime].
|
|
319
|
+
host_utc_current_time = datetime.fromtimestamp(
|
|
320
|
+
host.get_fact(Date).timestamp(), timezone.utc
|
|
321
|
+
).replace(tzinfo=None)
|
|
322
|
+
host_cache_time = host_utc_current_time - timedelta(seconds=cache_time)
|
|
323
|
+
if cache_info["mtime"] > host_cache_time:
|
|
324
|
+
host.noop("apt is already up to date")
|
|
325
|
+
return
|
|
318
326
|
|
|
319
327
|
yield "apt-get update"
|
|
320
328
|
|
pyinfra/operations/brew.py
CHANGED
pyinfra/operations/crontab.py
CHANGED
|
@@ -54,6 +54,7 @@ def crontab(
|
|
|
54
54
|
|
|
55
55
|
.. code:: python
|
|
56
56
|
|
|
57
|
+
from pyinfra.operations import crontab
|
|
57
58
|
# simple example for a crontab
|
|
58
59
|
crontab.crontab(
|
|
59
60
|
name="Backup /etc weekly",
|
|
@@ -84,7 +85,9 @@ def crontab(
|
|
|
84
85
|
ctb = ctb0
|
|
85
86
|
name_comment = "# pyinfra-name={0}".format(cron_name)
|
|
86
87
|
|
|
87
|
-
existing_crontab = ctb.get_command(
|
|
88
|
+
existing_crontab = ctb.get_command(
|
|
89
|
+
command=command if cron_name is None else None, name=cron_name
|
|
90
|
+
)
|
|
88
91
|
existing_crontab_command = existing_crontab["command"] if existing_crontab else command
|
|
89
92
|
existing_crontab_match = existing_crontab["command"] if existing_crontab else command
|
|
90
93
|
|
pyinfra/operations/dnf.py
CHANGED
|
@@ -25,9 +25,12 @@ def key(src: str):
|
|
|
25
25
|
|
|
26
26
|
.. code:: python
|
|
27
27
|
|
|
28
|
+
from pyinfra import host
|
|
29
|
+
from pyinfra.operations import dnf
|
|
30
|
+
from pyinfra.facts.server import LinuxDistribution
|
|
28
31
|
linux_id = host.get_fact(LinuxDistribution)["release_meta"].get("ID")
|
|
29
32
|
dnf.key(
|
|
30
|
-
name="Add the Docker
|
|
33
|
+
name="Add the Docker gpg key",
|
|
31
34
|
src=f"https://download.docker.com/linux/{linux_id}/gpg",
|
|
32
35
|
)
|
|
33
36
|
|
pyinfra/operations/docker.py
CHANGED
|
@@ -8,9 +8,15 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from pyinfra import host
|
|
10
10
|
from pyinfra.api import operation
|
|
11
|
-
from pyinfra.facts.docker import
|
|
11
|
+
from pyinfra.facts.docker import (
|
|
12
|
+
DockerContainer,
|
|
13
|
+
DockerImage,
|
|
14
|
+
DockerNetwork,
|
|
15
|
+
DockerPlugin,
|
|
16
|
+
DockerVolume,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
|
-
from .util.docker import ContainerSpec, handle_docker
|
|
19
|
+
from .util.docker import ContainerSpec, handle_docker, parse_image_reference
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
@operation()
|
|
@@ -21,10 +27,13 @@ def container(
|
|
|
21
27
|
networks: list[str] | None = None,
|
|
22
28
|
volumes: list[str] | None = None,
|
|
23
29
|
env_vars: list[str] | None = None,
|
|
30
|
+
labels: list[str] | None = None,
|
|
24
31
|
pull_always: bool = False,
|
|
25
32
|
present: bool = True,
|
|
26
33
|
force: bool = False,
|
|
27
34
|
start: bool = True,
|
|
35
|
+
restart_policy: str | None = None,
|
|
36
|
+
auto_remove: bool = False,
|
|
28
37
|
):
|
|
29
38
|
"""
|
|
30
39
|
Manage Docker containers
|
|
@@ -35,15 +44,19 @@ def container(
|
|
|
35
44
|
+ ports: port list to expose
|
|
36
45
|
+ volumes: volume list to map on container
|
|
37
46
|
+ env_vars: environment variable list to inject on container
|
|
47
|
+
+ labels: Label list to attach to the container
|
|
38
48
|
+ pull_always: force image pull
|
|
39
49
|
+ force: remove a container with same name and create a new one
|
|
40
50
|
+ present: whether the container should be up and running
|
|
41
51
|
+ start: start or stop the container
|
|
52
|
+
+ restart_policy: restart policy to apply when a container exits
|
|
53
|
+
+ auto_remove: automatically remove the container and its associated anonymous volumes when it exits
|
|
42
54
|
|
|
43
55
|
**Examples:**
|
|
44
56
|
|
|
45
57
|
.. code:: python
|
|
46
58
|
|
|
59
|
+
from pyinfra.operations import docker
|
|
47
60
|
# Run a container
|
|
48
61
|
docker.container(
|
|
49
62
|
name="Deploy Nginx container",
|
|
@@ -55,6 +68,8 @@ def container(
|
|
|
55
68
|
networks=["proxy", "services"],
|
|
56
69
|
volumes=["nginx_data:/usr/share/nginx/html"],
|
|
57
70
|
pull_always=True,
|
|
71
|
+
restart_policy="unless-stopped",
|
|
72
|
+
auto_remove=True,
|
|
58
73
|
)
|
|
59
74
|
|
|
60
75
|
# Stop a container
|
|
@@ -78,7 +93,10 @@ def container(
|
|
|
78
93
|
networks or list(),
|
|
79
94
|
volumes or list(),
|
|
80
95
|
env_vars or list(),
|
|
96
|
+
labels or list(),
|
|
81
97
|
pull_always,
|
|
98
|
+
restart_policy,
|
|
99
|
+
auto_remove,
|
|
82
100
|
)
|
|
83
101
|
existent_container = host.get_fact(DockerContainer, object_id=container)
|
|
84
102
|
|
|
@@ -127,13 +145,14 @@ def container(
|
|
|
127
145
|
)
|
|
128
146
|
|
|
129
147
|
|
|
130
|
-
@operation(
|
|
131
|
-
def image(image, present=True):
|
|
148
|
+
@operation()
|
|
149
|
+
def image(image: str, present: bool = True, force: bool = False):
|
|
132
150
|
"""
|
|
133
151
|
Manage Docker images
|
|
134
152
|
|
|
135
153
|
+ image: Image and tag ex: nginx:alpine
|
|
136
154
|
+ present: whether the Docker image should exist
|
|
155
|
+
+ force: always pull the image if present is True
|
|
137
156
|
|
|
138
157
|
**Examples:**
|
|
139
158
|
|
|
@@ -153,20 +172,55 @@ def image(image, present=True):
|
|
|
153
172
|
present=False,
|
|
154
173
|
)
|
|
155
174
|
"""
|
|
156
|
-
|
|
175
|
+
image_info = parse_image_reference(image)
|
|
157
176
|
if present:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
if force:
|
|
178
|
+
# always pull the image if force is True
|
|
179
|
+
yield handle_docker(
|
|
180
|
+
resource="image",
|
|
181
|
+
command="pull",
|
|
182
|
+
image=image,
|
|
183
|
+
)
|
|
184
|
+
return
|
|
185
|
+
else:
|
|
186
|
+
existent_image = host.get_fact(DockerImage, object_id=image)
|
|
187
|
+
if image_info.digest:
|
|
188
|
+
# If a digest is specified, we must ensure the exact image is present
|
|
189
|
+
if existent_image:
|
|
190
|
+
host.noop(f"Image with digest {image_info.digest} already exists!")
|
|
191
|
+
else:
|
|
192
|
+
yield handle_docker(
|
|
193
|
+
resource="image",
|
|
194
|
+
command="pull",
|
|
195
|
+
image=image,
|
|
196
|
+
)
|
|
197
|
+
elif image_info.tag == "latest" or not image_info.tag:
|
|
198
|
+
# If the tag is 'latest' or not specified, always pull to ensure freshness
|
|
199
|
+
yield handle_docker(
|
|
200
|
+
resource="image",
|
|
201
|
+
command="pull",
|
|
202
|
+
image=image,
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
# For other tags, check if the image exists
|
|
206
|
+
if existent_image:
|
|
207
|
+
host.noop(f"Image with tag {image_info.tag} already exists!")
|
|
208
|
+
else:
|
|
209
|
+
yield handle_docker(
|
|
210
|
+
resource="image",
|
|
211
|
+
command="pull",
|
|
212
|
+
image=image,
|
|
213
|
+
)
|
|
164
214
|
else:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
215
|
+
existent_image = host.get_fact(DockerImage, object_id=image)
|
|
216
|
+
if existent_image:
|
|
217
|
+
yield handle_docker(
|
|
218
|
+
resource="image",
|
|
219
|
+
command="remove",
|
|
220
|
+
image=image,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
host.noop("There is no {0} image!".format(image))
|
|
170
224
|
|
|
171
225
|
|
|
172
226
|
@operation()
|