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.
Files changed (55) hide show
  1. pyinfra/api/__init__.py +1 -0
  2. pyinfra/api/arguments.py +7 -0
  3. pyinfra/api/exceptions.py +6 -0
  4. pyinfra/api/facts.py +17 -1
  5. pyinfra/api/host.py +3 -0
  6. pyinfra/api/metadata.py +69 -0
  7. pyinfra/api/operations.py +3 -3
  8. pyinfra/api/util.py +22 -5
  9. pyinfra/connectors/docker.py +25 -1
  10. pyinfra/connectors/ssh.py +57 -0
  11. pyinfra/connectors/util.py +16 -9
  12. pyinfra/facts/crontab.py +7 -7
  13. pyinfra/facts/files.py +1 -2
  14. pyinfra/facts/npm.py +1 -1
  15. pyinfra/facts/server.py +18 -2
  16. pyinfra/operations/apk.py +2 -1
  17. pyinfra/operations/apt.py +15 -7
  18. pyinfra/operations/brew.py +1 -0
  19. pyinfra/operations/crontab.py +4 -1
  20. pyinfra/operations/dnf.py +4 -1
  21. pyinfra/operations/docker.py +62 -16
  22. pyinfra/operations/files.py +87 -12
  23. pyinfra/operations/flatpak.py +1 -0
  24. pyinfra/operations/gem.py +1 -0
  25. pyinfra/operations/git.py +1 -0
  26. pyinfra/operations/iptables.py +1 -0
  27. pyinfra/operations/lxd.py +1 -0
  28. pyinfra/operations/mysql.py +1 -0
  29. pyinfra/operations/opkg.py +2 -1
  30. pyinfra/operations/pacman.py +1 -0
  31. pyinfra/operations/pip.py +1 -0
  32. pyinfra/operations/pipx.py +1 -0
  33. pyinfra/operations/pkg.py +1 -0
  34. pyinfra/operations/pkgin.py +1 -0
  35. pyinfra/operations/postgres.py +1 -0
  36. pyinfra/operations/puppet.py +1 -0
  37. pyinfra/operations/python.py +1 -0
  38. pyinfra/operations/selinux.py +1 -0
  39. pyinfra/operations/server.py +1 -0
  40. pyinfra/operations/snap.py +2 -1
  41. pyinfra/operations/ssh.py +1 -0
  42. pyinfra/operations/systemd.py +1 -0
  43. pyinfra/operations/sysvinit.py +2 -1
  44. pyinfra/operations/util/docker.py +164 -8
  45. pyinfra/operations/util/packaging.py +2 -0
  46. pyinfra/operations/xbps.py +1 -0
  47. pyinfra/operations/yum.py +4 -1
  48. pyinfra/operations/zfs.py +1 -0
  49. pyinfra/operations/zypper.py +1 -0
  50. {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/METADATA +2 -1
  51. {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/RECORD +55 -54
  52. {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -1
  53. pyinfra_cli/cli.py +13 -0
  54. {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/entry_points.txt +0 -0
  55. {pyinfra-3.5.1.dist-info → pyinfra-3.6.dist-info}/licenses/LICENSE.md +0 -0
@@ -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 DockerContainer, DockerNetwork, DockerPlugin, DockerVolume
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(is_idempotent=False)
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
- yield handle_docker(
159
- resource="image",
160
- command="pull",
161
- image=image,
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
- yield handle_docker(
166
- resource="image",
167
- command="remove",
168
- image=image,
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()
@@ -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(temp_dir) if temp_dir is not None else None
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(src, dest, remote_temp_filename=host.get_temp_filename(dest))
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(src, dest, remote_temp_filename=host.get_temp_filename(dest))
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(src, dest, remote_temp_filename=host.get_temp_filename(dest))
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
- Common convention is to store templates in a "templates" directory and
1160
- have a filename suffix with '.j2' (for jinja2).
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
- For information on the template syntax, see
1163
- `the jinja2 docs <https://jinja.palletsprojects.com>`_.
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 $(thefuck -a)",
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=/tmp mktemp -t pyinfra.XXXXXX)" && ',
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,
@@ -26,6 +26,7 @@ def packages(
26
26
 
27
27
  .. code:: python
28
28
 
29
+ from pyinfra.operations import flatpak
29
30
  # Install vlc flatpak
30
31
  flatpak.package(
31
32
  name="Install vlc",
pyinfra/operations/gem.py CHANGED
@@ -27,6 +27,7 @@ def packages(packages: str | list[str] | None = None, present=True, latest=False
27
27
 
28
28
  .. code:: python
29
29
 
30
+ from pyinfra.operations import gem
30
31
  # Note: Assumes that 'gem' is installed.
31
32
  gem.packages(
32
33
  name="Install rspec",
pyinfra/operations/git.py CHANGED
@@ -30,6 +30,7 @@ def config(key: str, value: str, multi_value=False, repo: str | None = None, sys
30
30
 
31
31
  .. code:: python
32
32
 
33
+ from pyinfra.operations import git
33
34
  git.config(
34
35
  name="Always prune specified repo",
35
36
  key="fetch.prune",
@@ -118,6 +118,7 @@ def rule(
118
118
 
119
119
  .. code:: python
120
120
 
121
+ from pyinfra.operations import iptables
121
122
  iptables.rule(
122
123
  name="Block SSH traffic",
123
124
  chain="INPUT",
pyinfra/operations/lxd.py CHANGED
@@ -38,6 +38,7 @@ def container(
38
38
 
39
39
  .. code:: python
40
40
 
41
+ from pyinfra.operations import lxd
41
42
  lxd.container(
42
43
  name="Add an ubuntu container",
43
44
  id="ubuntu19",
@@ -101,6 +101,7 @@ def user(
101
101
 
102
102
  .. code:: python
103
103
 
104
+ from pyinfra.operations import mysql
104
105
  mysql.user(
105
106
  name="Create the pyinfra@localhost MySQL user",
106
107
  user="pyinfra",
@@ -52,7 +52,8 @@ def packages(
52
52
 
53
53
  .. code:: python
54
54
 
55
- # Ensure packages are installed∂ (will not force package upgrade)
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)
@@ -57,6 +57,7 @@ def packages(
57
57
 
58
58
  .. code:: python
59
59
 
60
+ from pyinfra.operations import pacman
60
61
  pacman.packages(
61
62
  name="Install Vim and a plugin",
62
63
  packages=["vim-fugitive", "vim"],
pyinfra/operations/pip.py CHANGED
@@ -36,6 +36,7 @@ def virtualenv(
36
36
 
37
37
  .. code:: python
38
38
 
39
+ from pyinfra.operations import pip
39
40
  pip.virtualenv(
40
41
  name="Create a virtualenv",
41
42
  path="/usr/local/bin/venv",
@@ -34,6 +34,7 @@ def packages(
34
34
 
35
35
  .. code:: python
36
36
 
37
+ from pyinfra.operations import pipx
37
38
  pipx.packages(
38
39
  name="Install ",
39
40
  packages=["pyinfra"],
pyinfra/operations/pkg.py CHANGED
@@ -35,6 +35,7 @@ def packages(packages: str | list[str] | None = None, present=True, pkg_path: st
35
35
 
36
36
  .. code:: python
37
37
 
38
+ from pyinfra.operations import pkg
38
39
  pkg.packages(
39
40
  name="Install Vim and Vim Addon Manager",
40
41
  packages=["vim-addon-manager", "vim"],
@@ -56,6 +56,7 @@ def packages(
56
56
 
57
57
  .. code:: python
58
58
 
59
+ from pyinfra.operations import pkgin
59
60
  # Update package list and install packages
60
61
  pkgin.packages(
61
62
  name="Install tmux and Vim",
@@ -96,6 +96,7 @@ def role(
96
96
 
97
97
  .. code:: python
98
98
 
99
+ from pyinfra.operations import postgresql
99
100
  postgresql.role(
100
101
  name="Create the pyinfra PostgreSQL role",
101
102
  role="pyinfra",
@@ -19,6 +19,7 @@ def agent(server: str | None = None, port: int | None = None):
19
19
 
20
20
  .. code:: python
21
21
 
22
+ from pyinfra.operations import puppet
22
23
  puppet.agent()
23
24
 
24
25
  # We also expect a return code of:
@@ -22,6 +22,7 @@ def call(function: Callable, *args, **kwargs):
22
22
 
23
23
  .. code:: python
24
24
 
25
+ from pyinfra.operations import python
25
26
  def my_callback(hello=None):
26
27
  command = 'echo hello'
27
28
  if hello:
@@ -39,6 +39,7 @@ def boolean(bool_name: str, value: Boolean, persistent=False):
39
39
 
40
40
  .. code:: python
41
41
 
42
+ from pyinfra.operations import selinux
42
43
  selinux.boolean(
43
44
  name='Allow Apache to connect to LDAP server',
44
45
  'httpd_can_network_connect',
@@ -68,6 +68,7 @@ def reboot(delay=10, interval=1, reboot_timeout=300):
68
68
 
69
69
  .. code:: python
70
70
 
71
+ from pyinfra.operations import server
71
72
  server.reboot(
72
73
  name="Reboot the server and wait to reconnect",
73
74
  delay=60,
@@ -32,7 +32,8 @@ def package(
32
32
 
33
33
  .. code:: python
34
34
 
35
- # Install vlc snap
35
+ from pyinfra.operations import snap
36
+ # Install vlc via snap
36
37
  snap.package(
37
38
  name="Install vlc",
38
39
  packages="vlc",
pyinfra/operations/ssh.py CHANGED
@@ -28,6 +28,7 @@ def keyscan(hostname: str, force=False, port=22):
28
28
 
29
29
  .. code:: python
30
30
 
31
+ from pyinfra.operations import ssh
31
32
  ssh.keyscan(
32
33
  name="Set add server two to known_hosts on one",
33
34
  hostname="two.example.com",
@@ -66,6 +66,7 @@ def service(
66
66
 
67
67
  .. code:: python
68
68
 
69
+ from pyinfra.operations import systemd
69
70
  systemd.service(
70
71
  name="Restart and enable the dnsmasq service",
71
72
  service="dnsmasq.service",
@@ -38,7 +38,7 @@ def service(
38
38
  support enabling/disabling services:
39
39
 
40
40
  + Ubuntu/Debian (``update-rc.d``)
41
- + CentOS/Fedora/RHEL (``chkconfig``)
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",