pyinfra 3.5.1__py3-none-any.whl → 3.6__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 +7 -0
- pyinfra/api/exceptions.py +6 -0
- pyinfra/api/facts.py +17 -1
- pyinfra/api/host.py +3 -0
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operations.py +3 -3
- pyinfra/api/util.py +22 -5
- pyinfra/connectors/docker.py +25 -1
- pyinfra/connectors/ssh.py +57 -0
- pyinfra/connectors/util.py +16 -9
- pyinfra/facts/crontab.py +7 -7
- pyinfra/facts/files.py +1 -2
- 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 +62 -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 +1 -0
- 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 +164 -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.dist-info}/METADATA +2 -1
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/RECORD +55 -54
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -1
- pyinfra_cli/cli.py +13 -0
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/licenses/LICENSE.md +0 -0
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,6 +27,7 @@ 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,
|
|
@@ -35,6 +42,7 @@ def container(
|
|
|
35
42
|
+ ports: port list to expose
|
|
36
43
|
+ volumes: volume list to map on container
|
|
37
44
|
+ env_vars: environment variable list to inject on container
|
|
45
|
+
+ labels: Label list to attach to the container
|
|
38
46
|
+ pull_always: force image pull
|
|
39
47
|
+ force: remove a container with same name and create a new one
|
|
40
48
|
+ present: whether the container should be up and running
|
|
@@ -44,6 +52,7 @@ def container(
|
|
|
44
52
|
|
|
45
53
|
.. code:: python
|
|
46
54
|
|
|
55
|
+
from pyinfra.operations import docker
|
|
47
56
|
# Run a container
|
|
48
57
|
docker.container(
|
|
49
58
|
name="Deploy Nginx container",
|
|
@@ -78,6 +87,7 @@ def container(
|
|
|
78
87
|
networks or list(),
|
|
79
88
|
volumes or list(),
|
|
80
89
|
env_vars or list(),
|
|
90
|
+
labels or list(),
|
|
81
91
|
pull_always,
|
|
82
92
|
)
|
|
83
93
|
existent_container = host.get_fact(DockerContainer, object_id=container)
|
|
@@ -127,13 +137,14 @@ def container(
|
|
|
127
137
|
)
|
|
128
138
|
|
|
129
139
|
|
|
130
|
-
@operation(
|
|
131
|
-
def image(image, present=True):
|
|
140
|
+
@operation()
|
|
141
|
+
def image(image: str, present: bool = True, force: bool = False):
|
|
132
142
|
"""
|
|
133
143
|
Manage Docker images
|
|
134
144
|
|
|
135
145
|
+ image: Image and tag ex: nginx:alpine
|
|
136
146
|
+ present: whether the Docker image should exist
|
|
147
|
+
+ force: always pull the image if present is True
|
|
137
148
|
|
|
138
149
|
**Examples:**
|
|
139
150
|
|
|
@@ -153,20 +164,55 @@ def image(image, present=True):
|
|
|
153
164
|
present=False,
|
|
154
165
|
)
|
|
155
166
|
"""
|
|
156
|
-
|
|
167
|
+
image_info = parse_image_reference(image)
|
|
157
168
|
if present:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
if force:
|
|
170
|
+
# always pull the image if force is True
|
|
171
|
+
yield handle_docker(
|
|
172
|
+
resource="image",
|
|
173
|
+
command="pull",
|
|
174
|
+
image=image,
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
else:
|
|
178
|
+
existent_image = host.get_fact(DockerImage, object_id=image)
|
|
179
|
+
if image_info.digest:
|
|
180
|
+
# If a digest is specified, we must ensure the exact image is present
|
|
181
|
+
if existent_image:
|
|
182
|
+
host.noop(f"Image with digest {image_info.digest} already exists!")
|
|
183
|
+
else:
|
|
184
|
+
yield handle_docker(
|
|
185
|
+
resource="image",
|
|
186
|
+
command="pull",
|
|
187
|
+
image=image,
|
|
188
|
+
)
|
|
189
|
+
elif image_info.tag == "latest" or not image_info.tag:
|
|
190
|
+
# If the tag is 'latest' or not specified, always pull to ensure freshness
|
|
191
|
+
yield handle_docker(
|
|
192
|
+
resource="image",
|
|
193
|
+
command="pull",
|
|
194
|
+
image=image,
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
# For other tags, check if the image exists
|
|
198
|
+
if existent_image:
|
|
199
|
+
host.noop(f"Image with tag {image_info.tag} already exists!")
|
|
200
|
+
else:
|
|
201
|
+
yield handle_docker(
|
|
202
|
+
resource="image",
|
|
203
|
+
command="pull",
|
|
204
|
+
image=image,
|
|
205
|
+
)
|
|
164
206
|
else:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
207
|
+
existent_image = host.get_fact(DockerImage, object_id=image)
|
|
208
|
+
if existent_image:
|
|
209
|
+
yield handle_docker(
|
|
210
|
+
resource="image",
|
|
211
|
+
command="remove",
|
|
212
|
+
image=image,
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
host.noop("There is no {0} image!".format(image))
|
|
170
216
|
|
|
171
217
|
|
|
172
218
|
@operation()
|
pyinfra/operations/files.py
CHANGED
|
@@ -117,6 +117,7 @@ def download(
|
|
|
117
117
|
|
|
118
118
|
.. code:: python
|
|
119
119
|
|
|
120
|
+
from pyinfra.operations import files
|
|
120
121
|
files.download(
|
|
121
122
|
name="Download the Docker repo file",
|
|
122
123
|
src="https://download.docker.com/linux/centos/docker-ce.repo",
|
|
@@ -167,8 +168,12 @@ def download(
|
|
|
167
168
|
|
|
168
169
|
# If we download, always do user/group/mode as SSH user may be different
|
|
169
170
|
if download:
|
|
171
|
+
# Use explicit temp_dir if provided, otherwise fall back to _temp_dir global argument
|
|
172
|
+
effective_temp_dir = temp_dir
|
|
173
|
+
if effective_temp_dir is None and host.current_op_global_arguments:
|
|
174
|
+
effective_temp_dir = host.current_op_global_arguments.get("_temp_dir")
|
|
170
175
|
temp_file = host.get_temp_filename(
|
|
171
|
-
dest, temp_directory=str(
|
|
176
|
+
dest, temp_directory=str(effective_temp_dir) if effective_temp_dir is not None else None
|
|
172
177
|
)
|
|
173
178
|
|
|
174
179
|
curl_args: list[Union[str, StringCommand]] = ["-sSLf"]
|
|
@@ -751,6 +756,15 @@ def _file_equal(local_path: str | IO[Any] | None, remote_path: str) -> bool:
|
|
|
751
756
|
return False
|
|
752
757
|
|
|
753
758
|
|
|
759
|
+
def _remote_file_equal(remote_path_a: str, remote_path_b: str) -> bool:
|
|
760
|
+
for fact in [Sha1File, Md5File, Sha256File]:
|
|
761
|
+
sum_a = host.get_fact(fact, path=remote_path_a)
|
|
762
|
+
sum_b = host.get_fact(fact, path=remote_path_b)
|
|
763
|
+
if sum_a and sum_b:
|
|
764
|
+
return sum_a == sum_b
|
|
765
|
+
return False
|
|
766
|
+
|
|
767
|
+
|
|
754
768
|
@operation(
|
|
755
769
|
# We don't (currently) cache the local state, so there's nothing we can
|
|
756
770
|
# update to flag the local file as present.
|
|
@@ -797,19 +811,32 @@ def get(
|
|
|
797
811
|
|
|
798
812
|
remote_file = host.get_fact(File, path=src)
|
|
799
813
|
|
|
814
|
+
# Use _temp_dir global argument if provided
|
|
815
|
+
temp_dir = None
|
|
816
|
+
if host.current_op_global_arguments:
|
|
817
|
+
temp_dir = host.current_op_global_arguments.get("_temp_dir")
|
|
818
|
+
|
|
800
819
|
# No remote file, so assume exists and download it "blind"
|
|
801
820
|
if not remote_file or force:
|
|
802
|
-
yield FileDownloadCommand(
|
|
821
|
+
yield FileDownloadCommand(
|
|
822
|
+
src, dest, remote_temp_filename=host.get_temp_filename(dest, temp_directory=temp_dir)
|
|
823
|
+
)
|
|
803
824
|
|
|
804
825
|
# No local file, so always download
|
|
805
826
|
elif not os.path.exists(dest):
|
|
806
|
-
yield FileDownloadCommand(
|
|
827
|
+
yield FileDownloadCommand(
|
|
828
|
+
src, dest, remote_temp_filename=host.get_temp_filename(dest, temp_directory=temp_dir)
|
|
829
|
+
)
|
|
807
830
|
|
|
808
831
|
# Remote file exists - check if it matches our local
|
|
809
832
|
else:
|
|
810
833
|
# Check hash sum, download if needed
|
|
811
834
|
if not _file_equal(dest, src):
|
|
812
|
-
yield FileDownloadCommand(
|
|
835
|
+
yield FileDownloadCommand(
|
|
836
|
+
src,
|
|
837
|
+
dest,
|
|
838
|
+
remote_temp_filename=host.get_temp_filename(dest, temp_directory=temp_dir),
|
|
839
|
+
)
|
|
813
840
|
else:
|
|
814
841
|
host.noop("file {0} has already been downloaded".format(dest))
|
|
815
842
|
|
|
@@ -998,6 +1025,11 @@ def put(
|
|
|
998
1025
|
if create_remote_dir:
|
|
999
1026
|
yield from _create_remote_dir(dest, user, group)
|
|
1000
1027
|
|
|
1028
|
+
# Use _temp_dir global argument if provided
|
|
1029
|
+
temp_dir = None
|
|
1030
|
+
if host.current_op_global_arguments:
|
|
1031
|
+
temp_dir = host.current_op_global_arguments.get("_temp_dir")
|
|
1032
|
+
|
|
1001
1033
|
# No remote file, always upload and user/group/mode if supplied
|
|
1002
1034
|
if not remote_file or force:
|
|
1003
1035
|
if state.config.DIFF:
|
|
@@ -1013,7 +1045,7 @@ def put(
|
|
|
1013
1045
|
yield FileUploadCommand(
|
|
1014
1046
|
local_file,
|
|
1015
1047
|
dest,
|
|
1016
|
-
remote_temp_filename=host.get_temp_filename(dest),
|
|
1048
|
+
remote_temp_filename=host.get_temp_filename(dest, temp_directory=temp_dir),
|
|
1017
1049
|
)
|
|
1018
1050
|
|
|
1019
1051
|
if user or group:
|
|
@@ -1060,7 +1092,7 @@ def put(
|
|
|
1060
1092
|
yield FileUploadCommand(
|
|
1061
1093
|
local_file,
|
|
1062
1094
|
dest,
|
|
1063
|
-
remote_temp_filename=host.get_temp_filename(dest),
|
|
1095
|
+
remote_temp_filename=host.get_temp_filename(dest, temp_directory=temp_dir),
|
|
1064
1096
|
)
|
|
1065
1097
|
|
|
1066
1098
|
if user or group:
|
|
@@ -1156,11 +1188,14 @@ def template(
|
|
|
1156
1188
|
if not set explicitly.
|
|
1157
1189
|
|
|
1158
1190
|
Notes:
|
|
1159
|
-
|
|
1160
|
-
|
|
1191
|
+
Common convention is to store templates in a "templates" directory and
|
|
1192
|
+
have a filename suffix with '.j2' (for jinja2).
|
|
1193
|
+
|
|
1194
|
+
The default template lookup directory (used with jinjas ``extends``, ``import`` and
|
|
1195
|
+
``include`` statements) is the current working directory.
|
|
1161
1196
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1197
|
+
For information on the template syntax, see
|
|
1198
|
+
`the jinja2 docs <https://jinja.palletsprojects.com>`_.
|
|
1164
1199
|
|
|
1165
1200
|
**Examples:**
|
|
1166
1201
|
|
|
@@ -1309,6 +1344,39 @@ def move(src: str, dest: str, overwrite=False):
|
|
|
1309
1344
|
yield StringCommand("mv", QuoteString(src), QuoteString(dest))
|
|
1310
1345
|
|
|
1311
1346
|
|
|
1347
|
+
@operation()
|
|
1348
|
+
def copy(src: str, dest: str, overwrite=False):
|
|
1349
|
+
"""
|
|
1350
|
+
Copy remote file/directory/link into remote directory
|
|
1351
|
+
|
|
1352
|
+
+ src: remote file/directory to copy
|
|
1353
|
+
+ dest: remote directory to copy `src` into
|
|
1354
|
+
+ overwrite: whether to overwrite dest, if present
|
|
1355
|
+
"""
|
|
1356
|
+
src_is_dir = host.get_fact(Directory, src)
|
|
1357
|
+
if not host.get_fact(File, src) and not src_is_dir:
|
|
1358
|
+
raise OperationError(f"src {src} does not exist")
|
|
1359
|
+
|
|
1360
|
+
if not host.get_fact(Directory, dest):
|
|
1361
|
+
raise OperationError(f"dest {dest} is not an existing directory")
|
|
1362
|
+
|
|
1363
|
+
dest_file_path = os.path.join(dest, os.path.basename(src))
|
|
1364
|
+
dest_file_exists = host.get_fact(File, dest_file_path)
|
|
1365
|
+
if dest_file_exists and not overwrite:
|
|
1366
|
+
if _remote_file_equal(src, dest_file_path):
|
|
1367
|
+
host.noop(f"{dest_file_path} already exists")
|
|
1368
|
+
return
|
|
1369
|
+
else:
|
|
1370
|
+
raise OperationError(f"{dest_file_path} already exists and is different than src")
|
|
1371
|
+
|
|
1372
|
+
cp_cmd = ["cp -r"]
|
|
1373
|
+
|
|
1374
|
+
if overwrite:
|
|
1375
|
+
cp_cmd.append("-f")
|
|
1376
|
+
|
|
1377
|
+
yield StringCommand(*cp_cmd, QuoteString(src), QuoteString(dest))
|
|
1378
|
+
|
|
1379
|
+
|
|
1312
1380
|
def _validate_path(path):
|
|
1313
1381
|
try:
|
|
1314
1382
|
return os.fspath(path)
|
|
@@ -1786,7 +1854,7 @@ def block(
|
|
|
1786
1854
|
# put complex alias into .zshrc
|
|
1787
1855
|
files.block(
|
|
1788
1856
|
path="/home/user/.zshrc",
|
|
1789
|
-
content="eval $(
|
|
1857
|
+
content="eval $(thef -a)",
|
|
1790
1858
|
try_prevent_shell_expansion=True,
|
|
1791
1859
|
marker="## {mark} ALIASES ##"
|
|
1792
1860
|
)
|
|
@@ -1800,6 +1868,13 @@ def block(
|
|
|
1800
1868
|
current = host.get_fact(Block, path=path, marker=marker, begin=begin, end=end)
|
|
1801
1869
|
cmd = None
|
|
1802
1870
|
|
|
1871
|
+
# Use _temp_dir global argument if provided, otherwise fall back to config
|
|
1872
|
+
tmp_dir = None
|
|
1873
|
+
if host.current_op_global_arguments:
|
|
1874
|
+
tmp_dir = host.current_op_global_arguments.get("_temp_dir")
|
|
1875
|
+
if not tmp_dir:
|
|
1876
|
+
tmp_dir = host.get_temp_dir_config()
|
|
1877
|
+
|
|
1803
1878
|
# standard awk doesn't have an "in-place edit" option so we write to a tempfile and
|
|
1804
1879
|
# if edits were successful move to dest i.e. we do: <out_prep> ... do some work ... <real_out>
|
|
1805
1880
|
q_path = QuoteString(path)
|
|
@@ -1815,7 +1890,7 @@ def block(
|
|
|
1815
1890
|
)
|
|
1816
1891
|
)
|
|
1817
1892
|
out_prep = StringCommand(
|
|
1818
|
-
'OUT="$(TMPDIR
|
|
1893
|
+
f'OUT="$(TMPDIR={tmp_dir} mktemp -t pyinfra.XXXXXX)" && ',
|
|
1819
1894
|
*mode_get,
|
|
1820
1895
|
'OWNER="$(stat -c "%u:%g"',
|
|
1821
1896
|
q_path,
|
pyinfra/operations/flatpak.py
CHANGED
pyinfra/operations/gem.py
CHANGED
pyinfra/operations/git.py
CHANGED
pyinfra/operations/iptables.py
CHANGED
pyinfra/operations/lxd.py
CHANGED
pyinfra/operations/mysql.py
CHANGED
pyinfra/operations/opkg.py
CHANGED
|
@@ -52,7 +52,8 @@ def packages(
|
|
|
52
52
|
|
|
53
53
|
.. code:: python
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
from pyinfra.operations import opkg
|
|
56
|
+
# Ensure packages are installed (will not force package upgrade)
|
|
56
57
|
opkg.packages(['asterisk', 'vim'], name="Install Asterisk and Vim")
|
|
57
58
|
|
|
58
59
|
# Install the latest versions of packages (always check)
|
pyinfra/operations/pacman.py
CHANGED
pyinfra/operations/pip.py
CHANGED
pyinfra/operations/pipx.py
CHANGED
pyinfra/operations/pkg.py
CHANGED
pyinfra/operations/pkgin.py
CHANGED
pyinfra/operations/postgres.py
CHANGED
pyinfra/operations/puppet.py
CHANGED
pyinfra/operations/python.py
CHANGED
pyinfra/operations/selinux.py
CHANGED
pyinfra/operations/server.py
CHANGED
pyinfra/operations/snap.py
CHANGED
pyinfra/operations/ssh.py
CHANGED
pyinfra/operations/systemd.py
CHANGED
pyinfra/operations/sysvinit.py
CHANGED
|
@@ -38,7 +38,7 @@ def service(
|
|
|
38
38
|
support enabling/disabling services:
|
|
39
39
|
|
|
40
40
|
+ Ubuntu/Debian (``update-rc.d``)
|
|
41
|
-
+
|
|
41
|
+
+ Fedora/RHEL (``chkconfig``)
|
|
42
42
|
+ Gentoo (``rc-update``)
|
|
43
43
|
|
|
44
44
|
For other distributions and more granular service control, see the
|
|
@@ -48,6 +48,7 @@ def service(
|
|
|
48
48
|
|
|
49
49
|
.. code:: python
|
|
50
50
|
|
|
51
|
+
from pyinfra.operations import sysvinit
|
|
51
52
|
sysvinit.service(
|
|
52
53
|
name="Restart and enable rsyslog",
|
|
53
54
|
service="rsyslog",
|