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/context.py
CHANGED
|
@@ -59,16 +59,22 @@ class ContextObject:
|
|
|
59
59
|
if key in ("_container", "_base_cls"):
|
|
60
60
|
return super().__setattr__(key, value)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
mod = self._get_module()
|
|
63
|
+
if mod is None:
|
|
63
64
|
raise TypeError("Cannot assign to context base module")
|
|
64
|
-
|
|
65
|
-
return setattr(self._get_module(), key, value)
|
|
65
|
+
return setattr(mod, key, value)
|
|
66
66
|
|
|
67
67
|
def __iter__(self):
|
|
68
|
-
|
|
68
|
+
mod = self._get_module()
|
|
69
|
+
if mod is None:
|
|
70
|
+
raise ValueError("Context not set")
|
|
71
|
+
return iter(mod)
|
|
69
72
|
|
|
70
73
|
def __len__(self):
|
|
71
|
-
|
|
74
|
+
mod = self._get_module()
|
|
75
|
+
if mod is None:
|
|
76
|
+
raise ValueError("Context not set")
|
|
77
|
+
return len(mod)
|
|
72
78
|
|
|
73
79
|
@override
|
|
74
80
|
def __eq__(self, other):
|
|
@@ -105,6 +111,9 @@ class ContextManager:
|
|
|
105
111
|
@contextmanager
|
|
106
112
|
def use(self, module):
|
|
107
113
|
old_module = self.get()
|
|
114
|
+
if old_module is module:
|
|
115
|
+
yield # if we're double-setting, nothing to do
|
|
116
|
+
return
|
|
108
117
|
self.set(module)
|
|
109
118
|
yield
|
|
110
119
|
self.set(old_module)
|
pyinfra/facts/brew.py
CHANGED
pyinfra/facts/docker.py
CHANGED
|
@@ -49,7 +49,9 @@ class DockerContainers(DockerFactBase):
|
|
|
49
49
|
|
|
50
50
|
@override
|
|
51
51
|
def command(self) -> str:
|
|
52
|
-
return "
|
|
52
|
+
return """
|
|
53
|
+
ids=$(docker ps -qa) && [ -n "$ids" ] && docker container inspect $ids || echo "[]"
|
|
54
|
+
""".strip()
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
class DockerImages(DockerFactBase):
|
|
@@ -59,7 +61,9 @@ class DockerImages(DockerFactBase):
|
|
|
59
61
|
|
|
60
62
|
@override
|
|
61
63
|
def command(self) -> str:
|
|
62
|
-
return "
|
|
64
|
+
return """
|
|
65
|
+
ids=$(docker images -q) && [ -n "$ids" ] && docker image inspect $ids || echo "[]"
|
|
66
|
+
""".strip()
|
|
63
67
|
|
|
64
68
|
|
|
65
69
|
class DockerNetworks(DockerFactBase):
|
pyinfra/facts/git.py
CHANGED
|
@@ -23,6 +23,16 @@ class GitBranch(GitFactBase):
|
|
|
23
23
|
return re.sub(r"(heads|tags)/", r"", "\n".join(output))
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class GitTag(GitFactBase):
|
|
27
|
+
@override
|
|
28
|
+
def command(self, repo) -> str:
|
|
29
|
+
return "! test -d {0} || (cd {0} && git tag)".format(repo)
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
def process(self, output):
|
|
33
|
+
return output
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
class GitConfig(GitFactBase):
|
|
27
37
|
default = dict
|
|
28
38
|
|
pyinfra/facts/hardware.py
CHANGED
pyinfra/facts/opkg.py
CHANGED
pyinfra/facts/server.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import shutil
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from tempfile import mkdtemp
|
|
8
|
-
from typing import Dict, List, Optional
|
|
9
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
9
10
|
|
|
10
11
|
from dateutil.parser import parse as parse_date
|
|
11
12
|
from distro import distro
|
|
12
13
|
from typing_extensions import TypedDict, override
|
|
13
14
|
|
|
15
|
+
from pyinfra import host
|
|
14
16
|
from pyinfra.api import FactBase, ShortFactBase
|
|
15
17
|
from pyinfra.api.util import try_int
|
|
16
18
|
from pyinfra.facts import crontab
|
|
@@ -205,40 +207,96 @@ class Mounts(FactBase[Dict[str, MountsDict]]):
|
|
|
205
207
|
default = dict
|
|
206
208
|
|
|
207
209
|
@override
|
|
208
|
-
def command(self):
|
|
209
|
-
|
|
210
|
+
def command(self) -> str:
|
|
211
|
+
self._kernel = host.get_fact(Kernel)
|
|
212
|
+
|
|
213
|
+
if self._kernel.strip() == "FreeBSD":
|
|
214
|
+
return "mount -p --libxo json"
|
|
215
|
+
else:
|
|
216
|
+
return "cat /proc/self/mountinfo"
|
|
210
217
|
|
|
211
218
|
@override
|
|
212
219
|
def process(self, output) -> dict[str, MountsDict]:
|
|
213
220
|
devices: dict[str, MountsDict] = {}
|
|
214
221
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
line = line[4:]
|
|
219
|
-
is_map = True
|
|
222
|
+
def unescape_octal(match: re.Match) -> str:
|
|
223
|
+
s = match.group(0)[1:] # skip the backslash
|
|
224
|
+
return chr(int(s, base=8))
|
|
220
225
|
|
|
221
|
-
|
|
226
|
+
def replace_octal(s: str) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Unescape strings encoded by linux's string_escape_mem with ESCAPE_OCTAL flag.
|
|
229
|
+
"""
|
|
230
|
+
return re.sub(r"\\[0-7]{3}", unescape_octal, s)
|
|
222
231
|
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
if self._kernel == "FreeBSD":
|
|
233
|
+
full_output = "\n".join(output)
|
|
234
|
+
json_output = json.loads(full_output)
|
|
235
|
+
mount_fstab = json_output["mount"]["fstab"]
|
|
225
236
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
options =
|
|
231
|
-
type_ = options.pop(0)
|
|
237
|
+
for entry in mount_fstab:
|
|
238
|
+
path = entry["mntpoint"]
|
|
239
|
+
type_ = entry["fstype"]
|
|
240
|
+
device = entry["device"]
|
|
241
|
+
options = [option.strip() for option in entry["opts"].split(",")]
|
|
232
242
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
devices[path] = {"device": device, "type": type_, "options": options}
|
|
244
|
+
|
|
245
|
+
return devices
|
|
246
|
+
|
|
247
|
+
for line in output:
|
|
248
|
+
# ignore mount ID, parent ID, major:minor, root
|
|
249
|
+
_, _, _, _, mount_point, mount_options, line = line.split(sep=" ", maxsplit=6)
|
|
250
|
+
|
|
251
|
+
# ignore optional tags "shared", "master", "propagate_from" and "unbindable"
|
|
252
|
+
while True:
|
|
253
|
+
optional, line = line.split(sep=" ", maxsplit=1)
|
|
254
|
+
if optional == "-":
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
fs_type, mount_source, super_options = line.split(sep=" ")
|
|
258
|
+
|
|
259
|
+
mount_options = mount_options.split(sep=",")
|
|
260
|
+
|
|
261
|
+
# escaped: mount_point, mount_source, super_options
|
|
262
|
+
# these strings can contain characters encoded in octal, e.g. '\054' for ','
|
|
263
|
+
mount_point = replace_octal(mount_point)
|
|
264
|
+
mount_source = replace_octal(mount_source)
|
|
265
|
+
|
|
266
|
+
# mount_options will override ro/rw and can be different than the super block options
|
|
267
|
+
# filter them, so they don't appear twice
|
|
268
|
+
super_options = [
|
|
269
|
+
replace_octal(opt)
|
|
270
|
+
for opt in super_options.split(sep=",")
|
|
271
|
+
if opt not in ["ro", "rw"]
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
devices[mount_point] = {
|
|
275
|
+
"device": mount_source,
|
|
276
|
+
"type": fs_type,
|
|
277
|
+
"options": mount_options + super_options,
|
|
237
278
|
}
|
|
238
279
|
|
|
239
280
|
return devices
|
|
240
281
|
|
|
241
282
|
|
|
283
|
+
class Port(FactBase[Tuple[str, int] | Tuple[None, None]]):
|
|
284
|
+
"""
|
|
285
|
+
Returns the process occuping a port and its PID
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
@override
|
|
289
|
+
def command(self, port: int) -> str:
|
|
290
|
+
return f"ss -lptnH 'src :{port}'"
|
|
291
|
+
|
|
292
|
+
@override
|
|
293
|
+
def process(self, output: Iterable[str]) -> Tuple[str, int] | Tuple[None, None]:
|
|
294
|
+
for line in output:
|
|
295
|
+
proc, pid = line.split('"')[1], int(line.split("pid=")[1].split(",")[0])
|
|
296
|
+
return (proc, pid)
|
|
297
|
+
return None, None
|
|
298
|
+
|
|
299
|
+
|
|
242
300
|
class KernelModules(FactBase):
|
|
243
301
|
"""
|
|
244
302
|
Returns a dictionary of kernel module name -> info.
|
|
@@ -470,10 +528,10 @@ class Users(FactBase):
|
|
|
470
528
|
for i in `cat /etc/passwd | cut -d: -f1`; do
|
|
471
529
|
ENTRY=`grep ^$i: /etc/passwd`;
|
|
472
530
|
LASTLOG=`(((lastlog -u $i || lastlogin $i) 2> /dev/null) | grep ^$i | tr -s ' ')`;
|
|
473
|
-
PASSWORD=`grep ^$i: /etc/shadow | cut -d: -f2`;
|
|
531
|
+
PASSWORD=`(grep ^$i: /etc/shadow || grep ^$i: /etc/master.passwd) 2> /dev/null | cut -d: -f2`;
|
|
474
532
|
echo "$ENTRY|`id -gn $i`|`id -Gn $i`|$LASTLOG|$PASSWORD";
|
|
475
533
|
done
|
|
476
|
-
""".strip()
|
|
534
|
+
""".strip() # noqa
|
|
477
535
|
|
|
478
536
|
default = dict
|
|
479
537
|
|
pyinfra/facts/systemd.py
CHANGED
|
@@ -64,7 +64,7 @@ class SystemdStatus(FactBase[Dict[str, bool]]):
|
|
|
64
64
|
default = dict
|
|
65
65
|
|
|
66
66
|
state_key = "SubState"
|
|
67
|
-
state_values = ["running", "waiting", "exited", "listening"]
|
|
67
|
+
state_values = ["running", "waiting", "exited", "listening", "mounted"]
|
|
68
68
|
|
|
69
69
|
@override
|
|
70
70
|
def command(
|
pyinfra/operations/crontab.py
CHANGED
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import shlex
|
|
4
4
|
|
|
5
|
-
from pyinfra import
|
|
6
|
-
from pyinfra.api import StringCommand
|
|
5
|
+
from pyinfra import logger
|
|
6
|
+
from pyinfra.api.command import StringCommand
|
|
7
|
+
from pyinfra.api.operation import operation
|
|
7
8
|
from pyinfra.api.util import try_int
|
|
9
|
+
from pyinfra.context import host
|
|
8
10
|
from pyinfra.facts.crontab import Crontab, CrontabFile
|
|
9
|
-
from pyinfra.operations.util.files import sed_replace
|
|
11
|
+
from pyinfra.operations.util.files import sed_delete, sed_replace
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@operation()
|
|
@@ -111,7 +113,7 @@ def crontab(
|
|
|
111
113
|
# Don't want the cron and it does exist? Remove the line
|
|
112
114
|
if not present and exists:
|
|
113
115
|
edit_commands.append(
|
|
114
|
-
|
|
116
|
+
sed_delete(
|
|
115
117
|
temp_filename,
|
|
116
118
|
existing_crontab_match,
|
|
117
119
|
"",
|
|
@@ -121,7 +123,7 @@ def crontab(
|
|
|
121
123
|
|
|
122
124
|
# Want the cron but it doesn't exist? Append the line
|
|
123
125
|
elif present and not exists:
|
|
124
|
-
|
|
126
|
+
logger.debug(f"present: {present}, exists: {exists}")
|
|
125
127
|
if ctb: # append a blank line if cron entries already exist
|
|
126
128
|
edit_commands.append("echo '' >> {0}".format(temp_filename))
|
|
127
129
|
if cron_name:
|
pyinfra/operations/docker.py
CHANGED
|
@@ -227,6 +227,7 @@ def network(
|
|
|
227
227
|
ipam_driver="",
|
|
228
228
|
subnet="",
|
|
229
229
|
scope="",
|
|
230
|
+
aux_addresses=None,
|
|
230
231
|
opts=None,
|
|
231
232
|
ipam_opts=None,
|
|
232
233
|
labels=None,
|
|
@@ -279,6 +280,7 @@ def network(
|
|
|
279
280
|
ipam_driver=ipam_driver,
|
|
280
281
|
subnet=subnet,
|
|
281
282
|
scope=scope,
|
|
283
|
+
aux_addresses=aux_addresses,
|
|
282
284
|
opts=opts,
|
|
283
285
|
ipam_opts=ipam_opts,
|
|
284
286
|
labels=labels,
|
pyinfra/operations/files.py
CHANGED
|
@@ -32,7 +32,9 @@ from pyinfra.api.command import make_formatted_string_command
|
|
|
32
32
|
from pyinfra.api.util import (
|
|
33
33
|
get_call_location,
|
|
34
34
|
get_file_io,
|
|
35
|
+
get_file_md5,
|
|
35
36
|
get_file_sha1,
|
|
37
|
+
get_file_sha256,
|
|
36
38
|
get_path_permissions_mode,
|
|
37
39
|
get_template,
|
|
38
40
|
memoize,
|
|
@@ -56,7 +58,14 @@ from pyinfra.facts.files import (
|
|
|
56
58
|
from pyinfra.facts.server import Date, Which
|
|
57
59
|
|
|
58
60
|
from .util import files as file_utils
|
|
59
|
-
from .util.files import
|
|
61
|
+
from .util.files import (
|
|
62
|
+
adjust_regex,
|
|
63
|
+
ensure_mode_int,
|
|
64
|
+
get_timestamp,
|
|
65
|
+
sed_delete,
|
|
66
|
+
sed_replace,
|
|
67
|
+
unix_path_join,
|
|
68
|
+
)
|
|
60
69
|
|
|
61
70
|
|
|
62
71
|
@operation()
|
|
@@ -75,6 +84,9 @@ def download(
|
|
|
75
84
|
headers: dict[str, str] | None = None,
|
|
76
85
|
insecure=False,
|
|
77
86
|
proxy: str | None = None,
|
|
87
|
+
temp_dir: str | Path | None = None,
|
|
88
|
+
extra_curl_args: dict[str, str] | None = None,
|
|
89
|
+
extra_wget_args: dict[str, str] | None = None,
|
|
78
90
|
):
|
|
79
91
|
"""
|
|
80
92
|
Download files from remote locations using ``curl`` or ``wget``.
|
|
@@ -93,6 +105,9 @@ def download(
|
|
|
93
105
|
+ headers: optional dictionary of headers to set for the HTTP request
|
|
94
106
|
+ insecure: disable SSL verification for the HTTP request
|
|
95
107
|
+ proxy: simple HTTP proxy through which we can download files, form `http://<yourproxy>:<port>`
|
|
108
|
+
+ temp_dir: use this custom temporary directory during the download
|
|
109
|
+
+ extra_curl_args: optional dictionary with custom arguments for curl
|
|
110
|
+
+ extra_wget_args: optional dictionary with custom arguments for wget
|
|
96
111
|
|
|
97
112
|
**Example:**
|
|
98
113
|
|
|
@@ -148,11 +163,21 @@ def download(
|
|
|
148
163
|
|
|
149
164
|
# If we download, always do user/group/mode as SSH user may be different
|
|
150
165
|
if download:
|
|
151
|
-
temp_file = host.get_temp_filename(
|
|
166
|
+
temp_file = host.get_temp_filename(
|
|
167
|
+
dest, temp_directory=str(temp_dir) if temp_dir is not None else None
|
|
168
|
+
)
|
|
152
169
|
|
|
153
170
|
curl_args: list[Union[str, StringCommand]] = ["-sSLf"]
|
|
154
171
|
wget_args: list[Union[str, StringCommand]] = ["-q"]
|
|
155
172
|
|
|
173
|
+
if extra_curl_args:
|
|
174
|
+
for key, value in extra_curl_args.items():
|
|
175
|
+
curl_args.append(StringCommand(key, QuoteString(value)))
|
|
176
|
+
|
|
177
|
+
if extra_wget_args:
|
|
178
|
+
for key, value in extra_wget_args.items():
|
|
179
|
+
wget_args.append(StringCommand(key, QuoteString(value)))
|
|
180
|
+
|
|
156
181
|
if proxy:
|
|
157
182
|
curl_args.append(f"--proxy {proxy}")
|
|
158
183
|
wget_args.append("-e use_proxy=yes")
|
|
@@ -423,9 +448,9 @@ def line(
|
|
|
423
448
|
else:
|
|
424
449
|
host.noop('line "{0}" exists in {1}'.format(replace or line, path))
|
|
425
450
|
|
|
426
|
-
# Line(s) exists and we want to remove them
|
|
451
|
+
# Line(s) exists and we want to remove them
|
|
427
452
|
elif present_lines and not present:
|
|
428
|
-
yield
|
|
453
|
+
yield sed_delete(
|
|
429
454
|
path,
|
|
430
455
|
match_line,
|
|
431
456
|
"",
|
|
@@ -710,6 +735,21 @@ def _create_remote_dir(remote_filename, user, group):
|
|
|
710
735
|
)
|
|
711
736
|
|
|
712
737
|
|
|
738
|
+
def _file_equal(local_path: str | IO[Any] | None, remote_path: str) -> bool:
|
|
739
|
+
if local_path is None:
|
|
740
|
+
return False
|
|
741
|
+
for fact, get_sum in [
|
|
742
|
+
(Sha1File, get_file_sha1),
|
|
743
|
+
(Md5File, get_file_md5),
|
|
744
|
+
(Sha256File, get_file_sha256),
|
|
745
|
+
]:
|
|
746
|
+
remote_sum = host.get_fact(fact, path=remote_path)
|
|
747
|
+
if remote_sum:
|
|
748
|
+
local_sum = get_sum(local_path)
|
|
749
|
+
return local_sum == remote_sum
|
|
750
|
+
return False
|
|
751
|
+
|
|
752
|
+
|
|
713
753
|
@operation(
|
|
714
754
|
# We don't (currently) cache the local state, so there's nothing we can
|
|
715
755
|
# update to flag the local file as present.
|
|
@@ -766,12 +806,11 @@ def get(
|
|
|
766
806
|
|
|
767
807
|
# Remote file exists - check if it matches our local
|
|
768
808
|
else:
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
# Check sha1sum, upload if needed
|
|
773
|
-
if local_sum != remote_sum:
|
|
809
|
+
# Check hash sum, download if needed
|
|
810
|
+
if not _file_equal(dest, src):
|
|
774
811
|
yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
|
|
812
|
+
else:
|
|
813
|
+
host.noop("file {0} has already been downloaded".format(dest))
|
|
775
814
|
|
|
776
815
|
|
|
777
816
|
@operation()
|
|
@@ -805,7 +844,9 @@ def put(
|
|
|
805
844
|
|
|
806
845
|
``mode``:
|
|
807
846
|
When set to ``True`` the permissions of the local file are applied to the
|
|
808
|
-
remote file after the upload is complete.
|
|
847
|
+
remote file after the upload is complete. If set to an octal value with
|
|
848
|
+
digits for at least user, group, and other, either as an ``int`` or
|
|
849
|
+
``str``, those permissions will be used.
|
|
809
850
|
|
|
810
851
|
``create_remote_dir``:
|
|
811
852
|
If the remote directory does not exist it will be created using the same
|
|
@@ -816,6 +857,11 @@ def put(
|
|
|
816
857
|
This operation is not suitable for large files as it may involve copying
|
|
817
858
|
the file before uploading it.
|
|
818
859
|
|
|
860
|
+
Currently, if the mode argument is anything other than a ``bool`` or a full
|
|
861
|
+
octal permission set and the remote file exists, the operation will always
|
|
862
|
+
behave as if the remote file does not match the specified permissions and
|
|
863
|
+
requires a change.
|
|
864
|
+
|
|
819
865
|
**Examples:**
|
|
820
866
|
|
|
821
867
|
.. code:: python
|
|
@@ -837,7 +883,7 @@ def put(
|
|
|
837
883
|
# Upload IO objects as-is
|
|
838
884
|
if hasattr(src, "read"):
|
|
839
885
|
local_file = src
|
|
840
|
-
|
|
886
|
+
local_sum_path = src
|
|
841
887
|
|
|
842
888
|
# Assume string filename
|
|
843
889
|
else:
|
|
@@ -849,9 +895,9 @@ def put(
|
|
|
849
895
|
local_file = src
|
|
850
896
|
|
|
851
897
|
if os.path.isfile(local_file):
|
|
852
|
-
|
|
898
|
+
local_sum_path = local_file
|
|
853
899
|
elif assume_exists:
|
|
854
|
-
|
|
900
|
+
local_sum_path = None
|
|
855
901
|
else:
|
|
856
902
|
raise IOError("No such file: {0}".format(local_file))
|
|
857
903
|
|
|
@@ -893,10 +939,7 @@ def put(
|
|
|
893
939
|
|
|
894
940
|
# File exists, check sum and check user/group/mode if supplied
|
|
895
941
|
else:
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
# Check sha1sum, upload if needed
|
|
899
|
-
if local_sum != remote_sum:
|
|
942
|
+
if not _file_equal(local_sum_path, dest):
|
|
900
943
|
yield FileUploadCommand(
|
|
901
944
|
local_file,
|
|
902
945
|
dest,
|
|
@@ -1612,12 +1655,12 @@ def block(
|
|
|
1612
1655
|
"2>/dev/null || stat -f %Lp",
|
|
1613
1656
|
q_path,
|
|
1614
1657
|
") $OUT && ",
|
|
1615
|
-
'(chown $(stat -c "%
|
|
1658
|
+
'(chown $(stat -c "%u:%g"',
|
|
1616
1659
|
q_path,
|
|
1617
|
-
"2>/dev/null
|
|
1618
|
-
'
|
|
1660
|
+
"2>/dev/null || ",
|
|
1661
|
+
'stat -f "%u:%g"',
|
|
1619
1662
|
q_path,
|
|
1620
|
-
') $OUT)
|
|
1663
|
+
'2>/dev/null ) $OUT) && mv "$OUT"',
|
|
1621
1664
|
q_path,
|
|
1622
1665
|
)
|
|
1623
1666
|
|
pyinfra/operations/flatpak.py
CHANGED
|
@@ -12,12 +12,14 @@ from pyinfra.facts.flatpak import FlatpakPackages
|
|
|
12
12
|
@operation()
|
|
13
13
|
def packages(
|
|
14
14
|
packages: str | list[str] | None = None,
|
|
15
|
+
remote: str | None = None,
|
|
15
16
|
present=True,
|
|
16
17
|
):
|
|
17
18
|
"""
|
|
18
19
|
Install/remove a flatpak package
|
|
19
20
|
|
|
20
21
|
+ packages: List of packages
|
|
22
|
+
+ remote: Source to install the application or runtime from
|
|
21
23
|
+ present: whether the package should be installed
|
|
22
24
|
|
|
23
25
|
**Examples:**
|
|
@@ -30,6 +32,13 @@ def packages(
|
|
|
30
32
|
packages="org.videolan.VLC",
|
|
31
33
|
)
|
|
32
34
|
|
|
35
|
+
# Install vlc flatpak from flathub
|
|
36
|
+
flatpak.package(
|
|
37
|
+
name="Install vlc",
|
|
38
|
+
packages="org.videolan.VLC",
|
|
39
|
+
remote="flathub",
|
|
40
|
+
)
|
|
41
|
+
|
|
33
42
|
# Install multiple flatpaks
|
|
34
43
|
flatpak.package(
|
|
35
44
|
name="Install vlc and kodi",
|
|
@@ -55,6 +64,12 @@ def packages(
|
|
|
55
64
|
install_packages = []
|
|
56
65
|
remove_packages = []
|
|
57
66
|
|
|
67
|
+
if remote is None:
|
|
68
|
+
remote = ""
|
|
69
|
+
else:
|
|
70
|
+
# ensure we have a space between the remote and packages
|
|
71
|
+
remote = remote.strip() + " "
|
|
72
|
+
|
|
58
73
|
for package in packages:
|
|
59
74
|
# it's installed
|
|
60
75
|
if package in flatpak_packages:
|
|
@@ -73,7 +88,7 @@ def packages(
|
|
|
73
88
|
host.noop(f"flatpak package {package} is not installed")
|
|
74
89
|
|
|
75
90
|
if install_packages:
|
|
76
|
-
yield "
|
|
91
|
+
yield f"flatpak install --noninteractive {remote}{' '.join(install_packages)}"
|
|
77
92
|
|
|
78
93
|
if remove_packages:
|
|
79
|
-
yield "
|
|
94
|
+
yield f"flatpak uninstall --noninteractive {' '.join(remove_packages)}"
|
pyinfra/operations/git.py
CHANGED
|
@@ -9,7 +9,7 @@ import re
|
|
|
9
9
|
from pyinfra import host
|
|
10
10
|
from pyinfra.api import OperationError, operation
|
|
11
11
|
from pyinfra.facts.files import Directory, File
|
|
12
|
-
from pyinfra.facts.git import GitBranch, GitConfig, GitTrackingBranch
|
|
12
|
+
from pyinfra.facts.git import GitBranch, GitConfig, GitTag, GitTrackingBranch
|
|
13
13
|
|
|
14
14
|
from . import files, ssh
|
|
15
15
|
from .util.files import chown, unix_path_join
|
|
@@ -144,10 +144,14 @@ def repo(
|
|
|
144
144
|
git_commands.append("clone {0} .".format(src))
|
|
145
145
|
# Ensuring existing repo
|
|
146
146
|
else:
|
|
147
|
+
is_tag = False
|
|
147
148
|
if branch and host.get_fact(GitBranch, repo=dest) != branch:
|
|
148
149
|
git_commands.append("fetch") # fetch to ensure we have the branch locally
|
|
149
150
|
git_commands.append("checkout {0}".format(branch))
|
|
150
|
-
if
|
|
151
|
+
if branch and branch in host.get_fact(GitTag, repo=dest):
|
|
152
|
+
git_commands.append("checkout {0}".format(branch))
|
|
153
|
+
is_tag = True
|
|
154
|
+
if pull and not is_tag:
|
|
151
155
|
if rebase:
|
|
152
156
|
git_commands.append("pull --rebase")
|
|
153
157
|
else:
|