pyinfra 3.4__py2.py3-none-any.whl → 3.5__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 +63 -1
- pyinfra/api/config.py +6 -0
- pyinfra/api/connect.py +19 -2
- pyinfra/api/operation.py +54 -1
- pyinfra/api/operations.py +119 -56
- pyinfra/api/state.py +10 -2
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +39 -7
- pyinfra/connectors/util.py +4 -0
- pyinfra/facts/dnf.py +8 -4
- pyinfra/facts/docker.py +28 -8
- pyinfra/facts/files.py +167 -26
- pyinfra/facts/server.py +55 -4
- pyinfra/facts/util/packaging.py +1 -0
- pyinfra/facts/yum.py +8 -4
- pyinfra/facts/zypper.py +3 -3
- pyinfra/operations/crontab.py +1 -1
- pyinfra/operations/docker.py +130 -29
- pyinfra/operations/files.py +162 -7
- pyinfra/operations/git.py +1 -1
- pyinfra/operations/openrc.py +13 -7
- pyinfra/operations/pip.py +6 -7
- pyinfra/operations/pipx.py +19 -7
- pyinfra/operations/util/docker.py +49 -1
- pyinfra/operations/util/files.py +70 -2
- pyinfra/operations/util/packaging.py +98 -55
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/METADATA +3 -3
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/RECORD +37 -35
- pyinfra_cli/main.py +39 -0
- pyinfra_cli/prints.py +4 -0
- tests/test_api/test_api_operations.py +348 -0
- tests/test_cli/test_cli.py +3 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/WHEEL +0 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/top_level.txt +0 -0
pyinfra/operations/docker.py
CHANGED
|
@@ -4,25 +4,27 @@ the view of the current inventory host. See the :doc:`../connectors/docker` to u
|
|
|
4
4
|
as inventory directly.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
7
9
|
from pyinfra import host
|
|
8
10
|
from pyinfra.api import operation
|
|
9
|
-
from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerVolume
|
|
11
|
+
from pyinfra.facts.docker import DockerContainer, DockerNetwork, DockerPlugin, DockerVolume
|
|
10
12
|
|
|
11
13
|
from .util.docker import ContainerSpec, handle_docker
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@operation()
|
|
15
17
|
def container(
|
|
16
|
-
container,
|
|
17
|
-
image="",
|
|
18
|
-
ports=None,
|
|
19
|
-
networks=None,
|
|
20
|
-
volumes=None,
|
|
21
|
-
env_vars=None,
|
|
22
|
-
pull_always=False,
|
|
23
|
-
present=True,
|
|
24
|
-
force=False,
|
|
25
|
-
start=True,
|
|
18
|
+
container: str,
|
|
19
|
+
image: str = "",
|
|
20
|
+
ports: list[str] | None = None,
|
|
21
|
+
networks: list[str] | None = None,
|
|
22
|
+
volumes: list[str] | None = None,
|
|
23
|
+
env_vars: list[str] | None = None,
|
|
24
|
+
pull_always: bool = False,
|
|
25
|
+
present: bool = True,
|
|
26
|
+
force: bool = False,
|
|
27
|
+
start: bool = True,
|
|
26
28
|
):
|
|
27
29
|
"""
|
|
28
30
|
Manage Docker containers
|
|
@@ -168,7 +170,7 @@ def image(image, present=True):
|
|
|
168
170
|
|
|
169
171
|
|
|
170
172
|
@operation()
|
|
171
|
-
def volume(volume, driver="", labels=None, present=True):
|
|
173
|
+
def volume(volume: str, driver: str = "", labels: list[str] | None = None, present: bool = True):
|
|
172
174
|
"""
|
|
173
175
|
Manage Docker volumes
|
|
174
176
|
|
|
@@ -220,20 +222,20 @@ def volume(volume, driver="", labels=None, present=True):
|
|
|
220
222
|
|
|
221
223
|
@operation()
|
|
222
224
|
def network(
|
|
223
|
-
network,
|
|
224
|
-
driver="",
|
|
225
|
-
gateway="",
|
|
226
|
-
ip_range="",
|
|
227
|
-
ipam_driver="",
|
|
228
|
-
subnet="",
|
|
229
|
-
scope="",
|
|
230
|
-
aux_addresses=None,
|
|
231
|
-
opts=None,
|
|
232
|
-
ipam_opts=None,
|
|
233
|
-
labels=None,
|
|
234
|
-
ingress=False,
|
|
235
|
-
attachable=False,
|
|
236
|
-
present=True,
|
|
225
|
+
network: str,
|
|
226
|
+
driver: str = "",
|
|
227
|
+
gateway: str = "",
|
|
228
|
+
ip_range: str = "",
|
|
229
|
+
ipam_driver: str = "",
|
|
230
|
+
subnet: str = "",
|
|
231
|
+
scope: str = "",
|
|
232
|
+
aux_addresses: dict[str, str] | None = None,
|
|
233
|
+
opts: list[str] | None = None,
|
|
234
|
+
ipam_opts: list[str] | None = None,
|
|
235
|
+
labels: list[str] | None = None,
|
|
236
|
+
ingress: bool = False,
|
|
237
|
+
attachable: bool = False,
|
|
238
|
+
present: bool = True,
|
|
237
239
|
):
|
|
238
240
|
"""
|
|
239
241
|
Manage docker networks
|
|
@@ -245,6 +247,7 @@ def network(
|
|
|
245
247
|
+ ipam_driver: IP Address Management Driver
|
|
246
248
|
+ subnet: Subnet in CIDR format that represents a network segment
|
|
247
249
|
+ scope: Control the network's scope
|
|
250
|
+
+ aux_addresses: named aux addresses for the network
|
|
248
251
|
+ opts: Set driver specific options
|
|
249
252
|
+ ipam_opts: Set IPAM driver specific options
|
|
250
253
|
+ labels: Label list to attach in the network
|
|
@@ -303,9 +306,9 @@ def network(
|
|
|
303
306
|
|
|
304
307
|
@operation(is_idempotent=False)
|
|
305
308
|
def prune(
|
|
306
|
-
all=False,
|
|
307
|
-
volumes=False,
|
|
308
|
-
filter="",
|
|
309
|
+
all: bool = False,
|
|
310
|
+
volumes: bool = False,
|
|
311
|
+
filter: str = "",
|
|
309
312
|
):
|
|
310
313
|
"""
|
|
311
314
|
Execute a docker system prune.
|
|
@@ -344,3 +347,101 @@ def prune(
|
|
|
344
347
|
volumes=volumes,
|
|
345
348
|
filter=filter,
|
|
346
349
|
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@operation()
|
|
353
|
+
def plugin(
|
|
354
|
+
plugin: str,
|
|
355
|
+
alias: str | None = None,
|
|
356
|
+
present: bool = True,
|
|
357
|
+
enabled: bool = True,
|
|
358
|
+
plugin_options: dict[str, str] | None = None,
|
|
359
|
+
):
|
|
360
|
+
"""
|
|
361
|
+
Manage Docker plugins
|
|
362
|
+
|
|
363
|
+
+ plugin: Plugin name
|
|
364
|
+
+ alias: Alias for the plugin (optional)
|
|
365
|
+
+ present: Whether the plugin should be installed
|
|
366
|
+
+ enabled: Whether the plugin should be enabled
|
|
367
|
+
+ plugin_options: Options to pass to the plugin
|
|
368
|
+
|
|
369
|
+
**Examples:**
|
|
370
|
+
|
|
371
|
+
.. code:: python
|
|
372
|
+
|
|
373
|
+
# Install and enable a Docker plugin
|
|
374
|
+
docker.plugin(
|
|
375
|
+
name="Install and enable a Docker plugin",
|
|
376
|
+
plugin="username/my-awesome-plugin:latest",
|
|
377
|
+
alias="my-plugin",
|
|
378
|
+
present=True,
|
|
379
|
+
enabled=True,
|
|
380
|
+
plugin_options={"option1": "value1", "option2": "value2"},
|
|
381
|
+
)
|
|
382
|
+
"""
|
|
383
|
+
plugin_name = alias if alias else plugin
|
|
384
|
+
existent_plugin = host.get_fact(DockerPlugin, object_id=plugin_name)
|
|
385
|
+
if existent_plugin:
|
|
386
|
+
existent_plugin = existent_plugin[0]
|
|
387
|
+
|
|
388
|
+
if present:
|
|
389
|
+
if existent_plugin:
|
|
390
|
+
plugin_options_different = (
|
|
391
|
+
plugin_options and existent_plugin["Settings"]["Env"] != plugin_options
|
|
392
|
+
)
|
|
393
|
+
if plugin_options_different:
|
|
394
|
+
# Update options on existing plugin
|
|
395
|
+
if existent_plugin["Enabled"]:
|
|
396
|
+
yield handle_docker(
|
|
397
|
+
resource="plugin",
|
|
398
|
+
command="disable",
|
|
399
|
+
plugin=plugin_name,
|
|
400
|
+
)
|
|
401
|
+
yield handle_docker(
|
|
402
|
+
resource="plugin",
|
|
403
|
+
command="set",
|
|
404
|
+
plugin=plugin_name,
|
|
405
|
+
enabled=enabled,
|
|
406
|
+
existent_options=existent_plugin["Settings"]["Env"],
|
|
407
|
+
required_options=plugin_options,
|
|
408
|
+
)
|
|
409
|
+
if enabled:
|
|
410
|
+
yield handle_docker(
|
|
411
|
+
resource="plugin",
|
|
412
|
+
command="enable",
|
|
413
|
+
plugin=plugin_name,
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
416
|
+
# Options are the same, check if enabled state is different
|
|
417
|
+
if existent_plugin["Enabled"] == enabled:
|
|
418
|
+
host.noop(
|
|
419
|
+
f"Plugin '{plugin_name}' is already installed with the same options "
|
|
420
|
+
f"and {'enabled' if enabled else 'disabled'}."
|
|
421
|
+
)
|
|
422
|
+
return
|
|
423
|
+
else:
|
|
424
|
+
command = "enable" if enabled else "disable"
|
|
425
|
+
yield handle_docker(
|
|
426
|
+
resource="plugin",
|
|
427
|
+
command=command,
|
|
428
|
+
plugin=plugin_name,
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
yield handle_docker(
|
|
432
|
+
resource="plugin",
|
|
433
|
+
command="install",
|
|
434
|
+
plugin=plugin,
|
|
435
|
+
alias=alias,
|
|
436
|
+
enabled=enabled,
|
|
437
|
+
plugin_options=plugin_options,
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
if not existent_plugin:
|
|
441
|
+
host.noop(f"Plugin '{plugin_name}' is not installed.")
|
|
442
|
+
return
|
|
443
|
+
yield handle_docker(
|
|
444
|
+
resource="plugin",
|
|
445
|
+
command="remove",
|
|
446
|
+
plugin=plugin_name,
|
|
447
|
+
)
|
pyinfra/operations/files.py
CHANGED
|
@@ -8,12 +8,13 @@ import os
|
|
|
8
8
|
import posixpath
|
|
9
9
|
import sys
|
|
10
10
|
import traceback
|
|
11
|
-
from datetime import timedelta
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
12
|
from fnmatch import fnmatch
|
|
13
13
|
from io import StringIO
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import IO, Any, Union
|
|
16
16
|
|
|
17
|
+
import click
|
|
17
18
|
from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError
|
|
18
19
|
|
|
19
20
|
from pyinfra import host, logger, state
|
|
@@ -46,6 +47,7 @@ from pyinfra.facts.files import (
|
|
|
46
47
|
Block,
|
|
47
48
|
Directory,
|
|
48
49
|
File,
|
|
50
|
+
FileContents,
|
|
49
51
|
FindFiles,
|
|
50
52
|
FindInFile,
|
|
51
53
|
Flags,
|
|
@@ -59,8 +61,10 @@ from pyinfra.facts.server import Date, Which
|
|
|
59
61
|
|
|
60
62
|
from .util import files as file_utils
|
|
61
63
|
from .util.files import (
|
|
64
|
+
MetadataTimeField,
|
|
62
65
|
adjust_regex,
|
|
63
66
|
ensure_mode_int,
|
|
67
|
+
generate_color_diff,
|
|
64
68
|
get_timestamp,
|
|
65
69
|
sed_delete,
|
|
66
70
|
sed_replace,
|
|
@@ -813,6 +817,56 @@ def get(
|
|
|
813
817
|
host.noop("file {0} has already been downloaded".format(dest))
|
|
814
818
|
|
|
815
819
|
|
|
820
|
+
def _canonicalize_timespec(field: MetadataTimeField, local_file, timespec):
|
|
821
|
+
if isinstance(timespec, datetime):
|
|
822
|
+
if not timespec.tzinfo:
|
|
823
|
+
# specify remote host timezone
|
|
824
|
+
timespec_with_tz = timespec.replace(tzinfo=host.get_fact(Date).tzinfo)
|
|
825
|
+
return timespec_with_tz
|
|
826
|
+
else:
|
|
827
|
+
return timespec
|
|
828
|
+
elif isinstance(timespec, bool) and timespec:
|
|
829
|
+
lf_ts = (
|
|
830
|
+
os.stat(local_file).st_atime
|
|
831
|
+
if field is MetadataTimeField.ATIME
|
|
832
|
+
else os.stat(local_file).st_mtime
|
|
833
|
+
)
|
|
834
|
+
return datetime.fromtimestamp(lf_ts, tz=timezone.utc)
|
|
835
|
+
else:
|
|
836
|
+
try:
|
|
837
|
+
isodatetime = datetime.fromisoformat(timespec)
|
|
838
|
+
if not isodatetime.tzinfo:
|
|
839
|
+
return isodatetime.replace(tzinfo=host.get_fact(Date).tzinfo)
|
|
840
|
+
else:
|
|
841
|
+
return isodatetime
|
|
842
|
+
except ValueError:
|
|
843
|
+
try:
|
|
844
|
+
timestamp = float(timespec)
|
|
845
|
+
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
846
|
+
except ValueError:
|
|
847
|
+
# verify there is a remote file matching path in timesrc
|
|
848
|
+
ref_file = host.get_fact(File, path=timespec)
|
|
849
|
+
if ref_file:
|
|
850
|
+
if field is MetadataTimeField.ATIME:
|
|
851
|
+
assert ref_file["atime"] is not None
|
|
852
|
+
return ref_file["atime"].replace(tzinfo=timezone.utc)
|
|
853
|
+
else:
|
|
854
|
+
assert ref_file["mtime"] is not None
|
|
855
|
+
return ref_file["mtime"].replace(tzinfo=timezone.utc)
|
|
856
|
+
else:
|
|
857
|
+
ValueError("Bad argument for `timesspec`: {0}".format(timespec))
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
# returns True for a visible difference in the second field between the datetime values
|
|
861
|
+
# in the ref's TZ
|
|
862
|
+
def _times_differ_in_s(ref, cand):
|
|
863
|
+
assert ref.tzinfo and cand.tzinfo
|
|
864
|
+
cand_in_ref_tz = cand.astimezone(ref.tzinfo)
|
|
865
|
+
return (abs((cand_in_ref_tz - ref).total_seconds()) >= 1.0) or (
|
|
866
|
+
ref.second != cand_in_ref_tz.second
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
816
870
|
@operation()
|
|
817
871
|
def put(
|
|
818
872
|
src: str | IO[Any],
|
|
@@ -824,6 +878,8 @@ def put(
|
|
|
824
878
|
create_remote_dir=True,
|
|
825
879
|
force=False,
|
|
826
880
|
assume_exists=False,
|
|
881
|
+
atime: datetime | float | int | str | bool | None = None,
|
|
882
|
+
mtime: datetime | float | int | str | bool | None = None,
|
|
827
883
|
):
|
|
828
884
|
"""
|
|
829
885
|
Upload a local file, or file-like object, to the remote system.
|
|
@@ -837,6 +893,8 @@ def put(
|
|
|
837
893
|
+ create_remote_dir: create the remote directory if it doesn't exist
|
|
838
894
|
+ force: always upload the file, even if the remote copy matches
|
|
839
895
|
+ assume_exists: whether to assume the local file exists
|
|
896
|
+
+ atime: value of atime the file should have, use ``True`` to match the local file
|
|
897
|
+
+ mtime: value of mtime the file should have, use ``True`` to match the local file
|
|
840
898
|
|
|
841
899
|
``dest``:
|
|
842
900
|
If this is a directory that already exists on the remote side, the local
|
|
@@ -853,7 +911,21 @@ def put(
|
|
|
853
911
|
user & group as passed to ``files.put``. The mode will *not* be copied over,
|
|
854
912
|
if this is required call ``files.directory`` separately.
|
|
855
913
|
|
|
856
|
-
|
|
914
|
+
``atime`` and ``mtime``:
|
|
915
|
+
When set to values other than ``False`` or ``None``, the respective metadata
|
|
916
|
+
fields on the remote file will updated accordingly. Timestamp values are
|
|
917
|
+
considered equivalent if the difference is less than one second and they have
|
|
918
|
+
the identical number in the seconds field. If set to ``True`` the local
|
|
919
|
+
file is the source of the value. Otherwise, these values can be provided as
|
|
920
|
+
``datetime`` objects, POSIX timestamps, or strings that can be parsed into
|
|
921
|
+
either of these date and time specifications. They can also be reference file
|
|
922
|
+
paths on the remote host, as with the ``-r`` argument to ``touch``. If a
|
|
923
|
+
``datetime`` argument has no ``tzinfo`` value (i.e., it is naive), it is
|
|
924
|
+
assumed to be in the remote host's local timezone. There is no shortcut for
|
|
925
|
+
setting both ``atime` and ``mtime`` values with a single time specification,
|
|
926
|
+
unlike the native ``touch`` command.
|
|
927
|
+
|
|
928
|
+
Notes:
|
|
857
929
|
This operation is not suitable for large files as it may involve copying
|
|
858
930
|
the file before uploading it.
|
|
859
931
|
|
|
@@ -862,6 +934,12 @@ def put(
|
|
|
862
934
|
behave as if the remote file does not match the specified permissions and
|
|
863
935
|
requires a change.
|
|
864
936
|
|
|
937
|
+
If the ``atime`` argument is set for a given file, unless the remote
|
|
938
|
+
filesystem is mounted ``noatime`` or ``relatime``, multiple runs of this
|
|
939
|
+
operation will trigger the change detection for that file, since the act of
|
|
940
|
+
reading and checksumming the file will cause the host OS to update the file's
|
|
941
|
+
``atime``.
|
|
942
|
+
|
|
865
943
|
**Examples:**
|
|
866
944
|
|
|
867
945
|
.. code:: python
|
|
@@ -925,6 +1003,16 @@ def put(
|
|
|
925
1003
|
|
|
926
1004
|
# No remote file, always upload and user/group/mode if supplied
|
|
927
1005
|
if not remote_file or force:
|
|
1006
|
+
if state.config.DIFF:
|
|
1007
|
+
host.log(f"Will create {click.style(dest, bold=True)}", logger.info)
|
|
1008
|
+
|
|
1009
|
+
with get_file_io(src, "r") as f:
|
|
1010
|
+
desired_lines = f.readlines()
|
|
1011
|
+
|
|
1012
|
+
for line in generate_color_diff([], desired_lines):
|
|
1013
|
+
logger.info(f" {line}")
|
|
1014
|
+
logger.info("")
|
|
1015
|
+
|
|
928
1016
|
yield FileUploadCommand(
|
|
929
1017
|
local_file,
|
|
930
1018
|
dest,
|
|
@@ -937,9 +1025,41 @@ def put(
|
|
|
937
1025
|
if mode:
|
|
938
1026
|
yield file_utils.chmod(dest, mode)
|
|
939
1027
|
|
|
940
|
-
|
|
1028
|
+
# do mtime before atime to ensure atime setting isn't undone by mtime setting
|
|
1029
|
+
if mtime:
|
|
1030
|
+
yield file_utils.touch(
|
|
1031
|
+
dest,
|
|
1032
|
+
MetadataTimeField.MTIME,
|
|
1033
|
+
_canonicalize_timespec(MetadataTimeField.MTIME, src, mtime),
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
if atime:
|
|
1037
|
+
yield file_utils.touch(
|
|
1038
|
+
dest,
|
|
1039
|
+
MetadataTimeField.ATIME,
|
|
1040
|
+
_canonicalize_timespec(MetadataTimeField.ATIME, src, atime),
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
# File exists, check sum and check user/group/mode/atime/mtime if supplied
|
|
941
1044
|
else:
|
|
942
1045
|
if not _file_equal(local_sum_path, dest):
|
|
1046
|
+
if state.config.DIFF:
|
|
1047
|
+
# Generate diff when contents change
|
|
1048
|
+
current_contents = host.get_fact(FileContents, path=dest)
|
|
1049
|
+
if current_contents:
|
|
1050
|
+
current_lines = [line + "\n" for line in current_contents]
|
|
1051
|
+
else:
|
|
1052
|
+
current_lines = []
|
|
1053
|
+
|
|
1054
|
+
host.log(f"Will modify {click.style(dest, bold=True)}", logger.info)
|
|
1055
|
+
|
|
1056
|
+
with get_file_io(src, "r") as f:
|
|
1057
|
+
desired_lines = f.readlines()
|
|
1058
|
+
|
|
1059
|
+
for line in generate_color_diff(current_lines, desired_lines):
|
|
1060
|
+
logger.info(f" {line}")
|
|
1061
|
+
logger.info("")
|
|
1062
|
+
|
|
943
1063
|
yield FileUploadCommand(
|
|
944
1064
|
local_file,
|
|
945
1065
|
dest,
|
|
@@ -952,6 +1072,20 @@ def put(
|
|
|
952
1072
|
if mode:
|
|
953
1073
|
yield file_utils.chmod(dest, mode)
|
|
954
1074
|
|
|
1075
|
+
if mtime:
|
|
1076
|
+
yield file_utils.touch(
|
|
1077
|
+
dest,
|
|
1078
|
+
MetadataTimeField.MTIME,
|
|
1079
|
+
_canonicalize_timespec(MetadataTimeField.MTIME, src, mtime),
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
if atime:
|
|
1083
|
+
yield file_utils.touch(
|
|
1084
|
+
dest,
|
|
1085
|
+
MetadataTimeField.ATIME,
|
|
1086
|
+
_canonicalize_timespec(MetadataTimeField.ATIME, src, atime),
|
|
1087
|
+
)
|
|
1088
|
+
|
|
955
1089
|
else:
|
|
956
1090
|
changed = False
|
|
957
1091
|
|
|
@@ -965,6 +1099,26 @@ def put(
|
|
|
965
1099
|
yield file_utils.chown(dest, user, group)
|
|
966
1100
|
changed = True
|
|
967
1101
|
|
|
1102
|
+
# Check mtime
|
|
1103
|
+
if mtime:
|
|
1104
|
+
canonical_mtime = _canonicalize_timespec(MetadataTimeField.MTIME, src, mtime)
|
|
1105
|
+
assert remote_file["mtime"] is not None
|
|
1106
|
+
if _times_differ_in_s(
|
|
1107
|
+
canonical_mtime, remote_file["mtime"].replace(tzinfo=timezone.utc)
|
|
1108
|
+
):
|
|
1109
|
+
yield file_utils.touch(dest, MetadataTimeField.MTIME, canonical_mtime)
|
|
1110
|
+
changed = True
|
|
1111
|
+
|
|
1112
|
+
# Check atime
|
|
1113
|
+
if atime:
|
|
1114
|
+
canonical_atime = _canonicalize_timespec(MetadataTimeField.ATIME, src, atime)
|
|
1115
|
+
assert remote_file["atime"] is not None
|
|
1116
|
+
if _times_differ_in_s(
|
|
1117
|
+
canonical_atime, remote_file["atime"].replace(tzinfo=timezone.utc)
|
|
1118
|
+
):
|
|
1119
|
+
yield file_utils.touch(dest, MetadataTimeField.ATIME, canonical_atime)
|
|
1120
|
+
changed = True
|
|
1121
|
+
|
|
968
1122
|
if not changed:
|
|
969
1123
|
host.noop("file {0} is already uploaded".format(dest))
|
|
970
1124
|
|
|
@@ -1594,7 +1748,7 @@ def block(
|
|
|
1594
1748
|
path="/etc/hosts",
|
|
1595
1749
|
content="10.0.0.1 mars-one",
|
|
1596
1750
|
before=True,
|
|
1597
|
-
|
|
1751
|
+
line=".*localhost",
|
|
1598
1752
|
)
|
|
1599
1753
|
|
|
1600
1754
|
# have two entries in /etc/host
|
|
@@ -1603,7 +1757,7 @@ def block(
|
|
|
1603
1757
|
path="/etc/hosts",
|
|
1604
1758
|
content="10.0.0.1 mars-one\\n10.0.0.2 mars-two",
|
|
1605
1759
|
before=True,
|
|
1606
|
-
|
|
1760
|
+
line=".*localhost",
|
|
1607
1761
|
)
|
|
1608
1762
|
|
|
1609
1763
|
# remove marked entry from /etc/hosts
|
|
@@ -1618,7 +1772,7 @@ def block(
|
|
|
1618
1772
|
name="add out of date warning to web page",
|
|
1619
1773
|
path="/var/www/html/something.html",
|
|
1620
1774
|
content= "<p>Warning: this page is out of date.</p>",
|
|
1621
|
-
|
|
1775
|
+
line=".*<body>.*",
|
|
1622
1776
|
after=True
|
|
1623
1777
|
marker="<!-- {mark} PYINFRA BLOCK -->",
|
|
1624
1778
|
)
|
|
@@ -1665,6 +1819,7 @@ def block(
|
|
|
1665
1819
|
)
|
|
1666
1820
|
|
|
1667
1821
|
current = host.get_fact(Block, path=path, marker=marker, begin=begin, end=end)
|
|
1822
|
+
# None means file didn't exist, empty list means marker was not found
|
|
1668
1823
|
cmd = None
|
|
1669
1824
|
if present:
|
|
1670
1825
|
if not content:
|
|
@@ -1706,7 +1861,7 @@ def block(
|
|
|
1706
1861
|
prog = (
|
|
1707
1862
|
'awk \'BEGIN {x=ARGV[2]; ARGV[2]=""} '
|
|
1708
1863
|
f"{print_after} f!=1 && /{regex}/ {{ print x; f=1}} "
|
|
1709
|
-
f"END {{if (f==0) print
|
|
1864
|
+
f"END {{if (f==0) print x }} {print_before}'"
|
|
1710
1865
|
)
|
|
1711
1866
|
cmd = StringCommand(
|
|
1712
1867
|
out_prep,
|
pyinfra/operations/git.py
CHANGED
|
@@ -148,7 +148,7 @@ def repo(
|
|
|
148
148
|
if branch and host.get_fact(GitBranch, repo=dest) != branch:
|
|
149
149
|
git_commands.append("fetch") # fetch to ensure we have the branch locally
|
|
150
150
|
git_commands.append("checkout {0}".format(branch))
|
|
151
|
-
if branch and branch in host.get_fact(GitTag, repo=dest):
|
|
151
|
+
if branch and branch in (host.get_fact(GitTag, repo=dest) or []):
|
|
152
152
|
git_commands.append("checkout {0}".format(branch))
|
|
153
153
|
is_tag = True
|
|
154
154
|
if pull and not is_tag:
|
pyinfra/operations/openrc.py
CHANGED
|
@@ -48,10 +48,16 @@ def service(
|
|
|
48
48
|
openrc_enabled = host.get_fact(OpenrcEnabled, runlevel=runlevel)
|
|
49
49
|
is_enabled = openrc_enabled.get(service, False)
|
|
50
50
|
|
|
51
|
-
if enabled
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
if enabled is True:
|
|
52
|
+
if not is_enabled:
|
|
53
|
+
yield "rc-update add {0} {1}".format(service, runlevel)
|
|
54
|
+
openrc_enabled[service] = True
|
|
55
|
+
else:
|
|
56
|
+
host.noop("service {0} is enabled".format(service))
|
|
57
|
+
|
|
58
|
+
if enabled is False:
|
|
59
|
+
if is_enabled:
|
|
60
|
+
yield "rc-update del {0} {1}".format(service, runlevel)
|
|
61
|
+
openrc_enabled[service] = False
|
|
62
|
+
else:
|
|
63
|
+
host.noop("service {0} is disabled".format(service))
|
pyinfra/operations/pip.py
CHANGED
|
@@ -11,7 +11,7 @@ from pyinfra.facts.files import File
|
|
|
11
11
|
from pyinfra.facts.pip import PipPackages
|
|
12
12
|
|
|
13
13
|
from . import files
|
|
14
|
-
from .util.packaging import ensure_packages
|
|
14
|
+
from .util.packaging import PkgInfo, ensure_packages
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@operation()
|
|
@@ -186,21 +186,20 @@ def packages(
|
|
|
186
186
|
|
|
187
187
|
# Handle passed in packages
|
|
188
188
|
if packages:
|
|
189
|
+
if isinstance(packages, str):
|
|
190
|
+
packages = [packages]
|
|
191
|
+
# PEP-0426 states that Python packages should be compared using lowercase, so lowercase the
|
|
192
|
+
# current packages. PkgInfo.from_pep508 takes care of the package name
|
|
189
193
|
current_packages = host.get_fact(PipPackages, pip=pip)
|
|
190
|
-
|
|
191
|
-
# PEP-0426 states that Python packages should be compared using lowercase, so lowercase both
|
|
192
|
-
# the input packages and the fact packages before comparison.
|
|
193
|
-
packages = [pkg.lower() for pkg in packages]
|
|
194
194
|
current_packages = {pkg.lower(): versions for pkg, versions in current_packages.items()}
|
|
195
195
|
|
|
196
196
|
yield from ensure_packages(
|
|
197
197
|
host,
|
|
198
|
-
packages,
|
|
198
|
+
list(filter(None, (PkgInfo.from_pep508(package) for package in packages))),
|
|
199
199
|
current_packages,
|
|
200
200
|
present,
|
|
201
201
|
install_command=install_command,
|
|
202
202
|
uninstall_command=uninstall_command,
|
|
203
203
|
upgrade_command=upgrade_command,
|
|
204
|
-
version_join="==",
|
|
205
204
|
latest=latest,
|
|
206
205
|
)
|
pyinfra/operations/pipx.py
CHANGED
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
Manage pipx (python) applications.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
5
7
|
from pyinfra import host
|
|
6
8
|
from pyinfra.api import operation
|
|
7
9
|
from pyinfra.facts.pipx import PipxEnvironment, PipxPackages
|
|
8
10
|
from pyinfra.facts.server import Path
|
|
9
11
|
|
|
10
|
-
from .util.packaging import ensure_packages
|
|
12
|
+
from .util.packaging import PkgInfo, ensure_packages
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@operation()
|
|
14
16
|
def packages(
|
|
15
|
-
packages=None,
|
|
17
|
+
packages: Optional[Union[str, list[str]]] = None,
|
|
16
18
|
present=True,
|
|
17
19
|
latest=False,
|
|
18
|
-
extra_args=None,
|
|
20
|
+
extra_args: Optional[str] = None,
|
|
19
21
|
):
|
|
20
22
|
"""
|
|
21
23
|
Install/remove/update pipx packages.
|
|
22
24
|
|
|
23
|
-
+ packages: list of packages to ensure
|
|
25
|
+
+ packages: list of packages (PEP-508 format) to ensure
|
|
24
26
|
+ present: whether the packages should be installed
|
|
25
27
|
+ latest: whether to upgrade packages without a specified version
|
|
26
28
|
+ extra_args: additional arguments to the pipx command
|
|
@@ -37,6 +39,9 @@ def packages(
|
|
|
37
39
|
packages=["pyinfra"],
|
|
38
40
|
)
|
|
39
41
|
"""
|
|
42
|
+
if packages is None:
|
|
43
|
+
host.noop("no package list provided to pipx.packages")
|
|
44
|
+
return
|
|
40
45
|
|
|
41
46
|
prep_install_command = ["pipx", "install"]
|
|
42
47
|
|
|
@@ -47,19 +52,26 @@ def packages(
|
|
|
47
52
|
uninstall_command = "pipx uninstall"
|
|
48
53
|
upgrade_command = "pipx upgrade"
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
# PEP-0426 states that Python packages should be compared using lowercase, so lowercase the
|
|
56
|
+
# current packages. PkgInfo.from_pep508 takes care of it for the package names
|
|
57
|
+
current_packages = {
|
|
58
|
+
pkg.lower(): version for pkg, version in host.get_fact(PipxPackages).items()
|
|
59
|
+
}
|
|
60
|
+
if isinstance(packages, str):
|
|
61
|
+
packages = [packages]
|
|
51
62
|
|
|
52
63
|
# pipx support only one package name at a time
|
|
53
64
|
for package in packages:
|
|
65
|
+
if (pkg_info := PkgInfo.from_pep508(package)) is None:
|
|
66
|
+
continue # from_pep508 logged a warning
|
|
54
67
|
yield from ensure_packages(
|
|
55
68
|
host,
|
|
56
|
-
[
|
|
69
|
+
[pkg_info],
|
|
57
70
|
current_packages,
|
|
58
71
|
present,
|
|
59
72
|
install_command=install_command,
|
|
60
73
|
uninstall_command=uninstall_command,
|
|
61
74
|
upgrade_command=upgrade_command,
|
|
62
|
-
version_join="==",
|
|
63
75
|
latest=latest,
|
|
64
76
|
)
|
|
65
77
|
|