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
@@ -1,16 +1,169 @@
1
- import dataclasses
2
- from typing import Any, Dict, List
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
3
 
4
4
  from pyinfra.api import OperationError
5
5
 
6
6
 
7
- @dataclasses.dataclass
7
+ @dataclass
8
+ class ImageReference:
9
+ """Represents a parsed Docker image reference."""
10
+
11
+ repository: str
12
+ namespace: str | None = None
13
+ tag: str | None = None
14
+ digest: str | None = None
15
+ registry_host: str | None = None
16
+ registry_port: int | None = None
17
+
18
+ @property
19
+ def registry(self) -> str | None:
20
+ """Get the full registry address (host:port)."""
21
+ if not self.registry_host:
22
+ return None
23
+ if self.registry_port:
24
+ return f"{self.registry_host}:{self.registry_port}"
25
+ return self.registry_host
26
+
27
+ @property
28
+ def name(self) -> str:
29
+ """Get the full image name without tag or digest."""
30
+ parts = []
31
+ if self.registry:
32
+ parts.append(self.registry)
33
+ if self.namespace:
34
+ parts.append(self.namespace)
35
+ parts.append(self.repository)
36
+ return "/".join(parts)
37
+
38
+ @property
39
+ def full_reference(self) -> str:
40
+ """Get the complete image reference string."""
41
+ ref = self.name
42
+ if self.tag:
43
+ ref += f":{self.tag}"
44
+ if self.digest:
45
+ ref += f"@{self.digest}"
46
+ return ref
47
+
48
+
49
+ def parse_registry(registry: str) -> tuple[str, int | None]:
50
+ """
51
+ Parse a registry string into host and port components.
52
+
53
+ Args:
54
+ registry: String like "registry.io:5000" or "registry.io"
55
+
56
+ Returns:
57
+ tuple: (host, port) where port is None if not specified
58
+
59
+ Raises:
60
+ ValueError: If port is specified but not a valid integer
61
+ """
62
+ if ":" in registry:
63
+ host, port_str = registry.rsplit(":", 1)
64
+ if port_str: # Only try to parse if port_str is not empty
65
+ try:
66
+ port = int(port_str)
67
+ if port < 0 or port > 65535:
68
+ raise ValueError(
69
+ f"Invalid port number: {port}. Port must be between 0 and 65535"
70
+ )
71
+ return host, port
72
+ except ValueError as e:
73
+ if "invalid literal" in str(e):
74
+ raise ValueError(
75
+ f"Invalid port in registry '{registry}': '{port_str}' is not a valid port number"
76
+ )
77
+ raise # Re-raise port range error
78
+ else:
79
+ # Empty port (e.g., "registry.io:")
80
+ raise ValueError(f"Invalid registry format '{registry}': port cannot be empty")
81
+ else:
82
+ return registry, None
83
+
84
+
85
+ def parse_image_reference(image: str) -> ImageReference:
86
+ """
87
+ Parse a Docker image reference into components.
88
+
89
+ Format: [HOST[:PORT]/]NAMESPACE/REPOSITORY[:TAG][@DIGEST]
90
+
91
+ Raises:
92
+ ValueError: If the image reference is empty or invalid
93
+ """
94
+ if not image or not image.strip():
95
+ raise ValueError("Image reference cannot be empty")
96
+
97
+ original = image.strip()
98
+ registry_host = None
99
+ registry_port = None
100
+ namespace = None
101
+ repository = None
102
+ tag = None
103
+ digest = None
104
+
105
+ # Extract digest first (format: name@digest)
106
+ if "@" in original:
107
+ original, digest = original.rsplit("@", 1)
108
+
109
+ # Extract tag (format: name:tag)
110
+ if ":" in original:
111
+ parts = original.split(":")
112
+ if len(parts) >= 2:
113
+ potential_tag = parts[-1]
114
+ # Tag cannot contain '/' - if it does, the colon is part of the registry, separating host and port
115
+ if "/" not in potential_tag:
116
+ original = ":".join(parts[:-1])
117
+ tag = potential_tag
118
+
119
+ # Split by '/' to separate registry/namespace/repository
120
+ parts = original.split("/")
121
+
122
+ if len(parts) == 1:
123
+ # Just repository name (e.g., "nginx")
124
+ repository = parts[0]
125
+ elif len(parts) == 2:
126
+ # Could be namespace/repository or registry/repository
127
+ if "." in parts[0] or ":" in parts[0]:
128
+ # Likely a registry (registry.io:5000/repo or registry.io/repo)
129
+ registry_host, registry_port = parse_registry(parts[0])
130
+ repository = parts[1]
131
+ else:
132
+ # Likely namespace/repository
133
+ namespace = parts[0]
134
+ repository = parts[1]
135
+ elif len(parts) >= 3:
136
+ # registry/namespace/repository or registry/nested/namespace/repository
137
+ registry_host, registry_port = parse_registry(parts[0])
138
+ namespace = "/".join(parts[1:-1])
139
+ repository = parts[-1]
140
+
141
+ # Validate that we found a repository
142
+ if not repository:
143
+ raise ValueError(f"Invalid image reference: no repository found in '{image}'")
144
+
145
+ # Default tag to 'latest' if neither tag nor digest specified. This is Docker's default behavior.
146
+ if tag is None and digest is None:
147
+ tag = "latest"
148
+
149
+ return ImageReference(
150
+ repository=repository,
151
+ namespace=namespace,
152
+ tag=tag,
153
+ digest=digest,
154
+ registry_host=registry_host,
155
+ registry_port=registry_port,
156
+ )
157
+
158
+
159
+ @dataclass
8
160
  class ContainerSpec:
9
161
  image: str = ""
10
- ports: List[str] = dataclasses.field(default_factory=list)
11
- networks: List[str] = dataclasses.field(default_factory=list)
12
- volumes: List[str] = dataclasses.field(default_factory=list)
13
- env_vars: List[str] = dataclasses.field(default_factory=list)
162
+ ports: list[str] = field(default_factory=list)
163
+ networks: list[str] = field(default_factory=list)
164
+ volumes: list[str] = field(default_factory=list)
165
+ env_vars: list[str] = field(default_factory=list)
166
+ labels: list[str] = field(default_factory=list)
14
167
  pull_always: bool = False
15
168
 
16
169
  def container_create_args(self):
@@ -27,6 +180,9 @@ class ContainerSpec:
27
180
  for env_var in self.env_vars:
28
181
  args.append("-e {0}".format(env_var))
29
182
 
183
+ for label in self.labels:
184
+ args.append("--label {0}".format(label))
185
+
30
186
  if self.pull_always:
31
187
  args.append("--pull always")
32
188
 
@@ -34,7 +190,7 @@ class ContainerSpec:
34
190
 
35
191
  return args
36
192
 
37
- def diff_from_inspect(self, inspect_dict: Dict[str, Any]) -> List[str]:
193
+ def diff_from_inspect(self, inspect_dict: dict[str, Any]) -> list[str]:
38
194
  # TODO(@minor-fixes): Diff output of "docker inspect" against this spec
39
195
  # to determine if the container needs to be recreated. Currently, this
40
196
  # function will never recreate when attributes change, which is
@@ -172,6 +172,8 @@ def ensure_packages(
172
172
  return
173
173
  if isinstance(packages_to_ensure, str):
174
174
  packages_to_ensure = [packages_to_ensure]
175
+ if len(packages_to_ensure) == 0:
176
+ return
175
177
 
176
178
  packages: list[PkgInfo] = []
177
179
  if isinstance(packages_to_ensure[0], PkgInfo):
@@ -54,6 +54,7 @@ def packages(
54
54
 
55
55
  .. code:: python
56
56
 
57
+ from pyinfra.operations import xbps
57
58
  xbps.packages(
58
59
  name="Install Vim and Vim Pager",
59
60
  packages=["vimpager", "vim"],
pyinfra/operations/yum.py CHANGED
@@ -25,6 +25,9 @@ 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
  yum.key(
30
33
  name="Add the Docker CentOS gpg key",
@@ -75,7 +78,7 @@ def repo(
75
78
 
76
79
  # Create the repository file from baseurl/etc
77
80
  yum.repo(
78
- name="Add the Docker CentOS repo",
81
+ name="Add the Docker repo",
79
82
  src="DockerCE",
80
83
  baseurl="https://download.docker.com/linux/centos/7/$basearch/stable",
81
84
  )
pyinfra/operations/zfs.py CHANGED
@@ -33,6 +33,7 @@ def dataset(
33
33
 
34
34
  .. code:: python
35
35
 
36
+ from pyinfra.operations import zfs
36
37
  zfs.dataset(
37
38
  "tank/srv",
38
39
  mountpoint="/srv",
@@ -42,6 +42,7 @@ def repo(
42
42
 
43
43
  .. code:: python
44
44
 
45
+ from pyinfra.operations import zypper
45
46
  # Download a repository file
46
47
  zypper.repo(
47
48
  name="Install container virtualization repo via URL",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyinfra
3
- Version: 3.5.1
3
+ Version: 3.6
4
4
  Summary: pyinfra automates/provisions/manages/deploys infrastructure.
5
5
  Project-URL: homepage, https://pyinfra.com
6
6
  Project-URL: documentation, https://docs.pyinfra.com
@@ -29,6 +29,7 @@ Requires-Dist: gevent>=1.5
29
29
  Requires-Dist: jinja2<4,>3
30
30
  Requires-Dist: packaging>=16.1
31
31
  Requires-Dist: paramiko<4,>=2.7
32
+ Requires-Dist: pydantic<3,>=2.11
32
33
  Requires-Dist: python-dateutil<3,>2
33
34
  Requires-Dist: typeguard<5,>=4
34
35
  Requires-Dist: typing-extensions; python_version < '3.11'
@@ -5,32 +5,33 @@ pyinfra/local.py,sha256=wT84xkJc9UBN5isvIVbNpm2fzZaadwE-dkbAwaFQdZk,2808
5
5
  pyinfra/progress.py,sha256=X3hXZ4Flh_L9FE4ZEWxWoG0R4dA5UPd1FCO-Exd5Xtc,4193
6
6
  pyinfra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  pyinfra/version.py,sha256=LZf50PHDzEZv65w0G-iMICoQ9US0U5LWHAOEmNtkF3I,216
8
- pyinfra/api/__init__.py,sha256=suGbKKM-qCduuXFYBEcyswlTqozewtYpdLRhK63PVn0,942
9
- pyinfra/api/arguments.py,sha256=pxfizvECzEahbIyPfmVnk9FsQFnOnDIFPhBZvFLEhFg,12231
8
+ pyinfra/api/__init__.py,sha256=89brynFpoKWCE513go2pK2wfbGaU1V1gajX3UMm9LVA,964
9
+ pyinfra/api/arguments.py,sha256=mG6A2JgDL6RSFIibjKaocVxbWuFZEtzJ_Is8r6OZ4Mc,12438
10
10
  pyinfra/api/arguments_typed.py,sha256=IZuhpmDfW9CP6ASS5Ie-3Wcnxl6bDNR3egU4Mfhbsb4,2303
11
11
  pyinfra/api/command.py,sha256=NwF2syxV3zxCbBdHzvJ6ve5G-xwdNTjPHFPwguKVcYs,7741
12
12
  pyinfra/api/config.py,sha256=gVDV-aGh6LYOnHtBaivICrd3RBfjFRWy3-K9sG__eP8,9321
13
13
  pyinfra/api/connect.py,sha256=jkx07iUL29u9pHHKH4WcNtvxwOA4DIbF7ixguFyuFjo,1984
14
14
  pyinfra/api/connectors.py,sha256=nie7JuLxMSC6gqPjmjuCisQ11R-eAQDtMMWF6YbSQ48,659
15
15
  pyinfra/api/deploy.py,sha256=Upd92oThQN0zhKbKW8vyPvBuoYiEGStuiEy7kNhZ00Y,3167
16
- pyinfra/api/exceptions.py,sha256=cCbUp1qN1QO0d9aAvOAbRgYpLi0vUI5j7ZqSjcD1_P8,1861
17
- pyinfra/api/facts.py,sha256=nybuKLncxHrJ7rHT2JH0Bg7YwHjQWb-fc8ABJ1J9Y4I,9278
18
- pyinfra/api/host.py,sha256=4a1bFR8vX8mUuXRZttXlzp2_ARzrg-xsH7n8uOxaaqQ,14098
16
+ pyinfra/api/exceptions.py,sha256=dp11JB5OUn48slAldVN8l8m8F8DpUXULIy0-VonzMS0,2004
17
+ pyinfra/api/facts.py,sha256=kT9KPKnDoBnz8Gck21yYDuYXzRzHio-B5ZwSqyj89Iw,9885
18
+ pyinfra/api/host.py,sha256=192rj8fsrHXudfnxzPlYxljXU24pReeWjXtrcCe9Kj4,14214
19
19
  pyinfra/api/inventory.py,sha256=i_LBI-Gn5FF-9SVDBH6xefTtvFzjuz12QQiFPGK2TrQ,7864
20
+ pyinfra/api/metadata.py,sha256=73BjwxKKA4mgP7D60K7Z8YIwPC11YN4IXaq26d209BI,1884
20
21
  pyinfra/api/operation.py,sha256=WtZyVwPoh9_AytqcCf_W6sMHhWEfSex9RE54aMBHU5M,17053
21
- pyinfra/api/operations.py,sha256=MedR-5W4BF23p_FMI7o62K72pgE7G1z2scKeC-b3JF0,13677
22
+ pyinfra/api/operations.py,sha256=JEVLcLeVO81PXd4Vmvn3XuAovZguURoNXBexYQWBnyo,13718
22
23
  pyinfra/api/state.py,sha256=cj-JvxOljeDshWvRpq8AMQxdGaUaht8KyuyR3mEsI-Y,12859
23
- pyinfra/api/util.py,sha256=N13IPSConC5XhJCDtBB5s-PC39B-PPWaujrzY3Pm2a0,12837
24
+ pyinfra/api/util.py,sha256=fwlgiFGFpHPQQ6BVT3SRDDunZBS3fZ7ApTEFuRI6r5M,13276
24
25
  pyinfra/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  pyinfra/connectors/base.py,sha256=0-BKInwTpjbnTakJhMTn_8LUOl81vUmC-q-HzVrwhkw,4703
26
27
  pyinfra/connectors/chroot.py,sha256=dBgruen5vS4j-OEFwF6ew-LPBR4kJEIQw_W5nXqlvVg,6064
27
- pyinfra/connectors/docker.py,sha256=QweBHSE5pRP5UuRp97xIDwr_tduBf88JHpSmgwU5SpM,11930
28
+ pyinfra/connectors/docker.py,sha256=BgSPsiqTY1SxchnSSPsuYNEA8YdWo6KD57h9TIE8YQQ,12679
28
29
  pyinfra/connectors/dockerssh.py,sha256=NGG6hSZ3z66RRKt6T0hnG2jiXFLR4P5uudi1COcGnY0,9021
29
30
  pyinfra/connectors/local.py,sha256=DJMmZiPgeYvZyW0ANvWjCxF3elmDlmolkY0ktTW3rcg,6990
30
- pyinfra/connectors/ssh.py,sha256=qAr_4To6llbj_lW2uVztzNj_aSWP_i-2iAjNN70Wf2E,22572
31
+ pyinfra/connectors/ssh.py,sha256=zESbhrA8Vp1FuzLdaeu21oHaBwpnYfK4VnbIFLo8oUY,24299
31
32
  pyinfra/connectors/ssh_util.py,sha256=CN_5AdTA3RpiWCnXTrRBjez1NsN59hITDzQmXIkZvoE,3683
32
33
  pyinfra/connectors/terraform.py,sha256=j-a2yStBLdw1QLZRVnl_9TanDtdyujyCxBO2Oa00rPM,4289
33
- pyinfra/connectors/util.py,sha256=-Ei6FMIzbVHD7G1R834Tg7RLoNzo-62wUYIkj043MRQ,11780
34
+ pyinfra/connectors/util.py,sha256=XJamDcmPLhTQQIycXUuUovi4herse-ZEI_rJcl-yskE,12029
34
35
  pyinfra/connectors/vagrant.py,sha256=0TT73ks64I4Yl-JSZjMBbpWA3VYBkqqLB-fUS8pS8GY,4813
35
36
  pyinfra/connectors/scp/__init__.py,sha256=jnO-_8GfkKWhsFcDjAxjOkuUT2RbS22b8P_xPrX889U,44
36
37
  pyinfra/connectors/scp/client.py,sha256=l_fPsbgz-7U6Y50ssuKKPFxD_cFoIPtaVXMCYDotbDI,6399
@@ -44,12 +45,12 @@ pyinfra/facts/brew.py,sha256=nE6YVc2S9zasyJPZmPR5FMeGKPViZYEcpnnBQlDf1EU,2792
44
45
  pyinfra/facts/bsdinit.py,sha256=SVY4hagjyy1yz8FKWhIbX9fHm5AugvTFl4xQh2FFO74,631
45
46
  pyinfra/facts/cargo.py,sha256=qgOClhwZm4COcncDzOZccCzs67nPBi_x6VGiF2UA0sA,687
46
47
  pyinfra/facts/choco.py,sha256=mpLleSqNqiaGRgyrhgceno2iPB1_1yjn8UJ90pvOZCs,886
47
- pyinfra/facts/crontab.py,sha256=yCpBzBqgXt2BjAGCIttuQ2xKKtuhzg0VQ9WCDV46eV8,5754
48
+ pyinfra/facts/crontab.py,sha256=mEY7bWvvjir23aUhNwm1mjCmFYHjVsWqrgbAXu1wt3M,5774
48
49
  pyinfra/facts/deb.py,sha256=1dR1puwY5wyyhhYYwaEBLjKU9sIyaNBNBlamVZ2KQg0,2074
49
50
  pyinfra/facts/dnf.py,sha256=wXatfZWVrrdLY7LM-vHKMg8Md1FiwkqHxmgRYbQqw90,1208
50
51
  pyinfra/facts/docker.py,sha256=fqIqMR6HwSYpTUAjhCX8Hk57pcyL6ShIl98H32Ly6HM,3233
51
52
  pyinfra/facts/efibootmgr.py,sha256=JPJSokE_RV9JstEPJRECnqSU-B0JCxmrocY8zBOva7M,3555
52
- pyinfra/facts/files.py,sha256=rZi7e6E3oq5XBzkh375ztHya5fHusrTbU7B3rZDbHrQ,19501
53
+ pyinfra/facts/files.py,sha256=xLMXbmYQ8x6Cri44Nh9BOGU0o5OAX39BtXzoOgoZyMM,19462
53
54
  pyinfra/facts/flatpak.py,sha256=ovi3duwTqqwvt5GoDRN7R-PpkvR6sQ1SmgEADcSnkUE,1646
54
55
  pyinfra/facts/freebsd.py,sha256=za42Di2M2-hcSTPei1YE6BsJxqapm9jysshs9hKJZaY,2161
55
56
  pyinfra/facts/gem.py,sha256=aX2-vcEqkxUIP0UJ_SVp9bf4B944oyDjsuujjs5q_9w,654
@@ -60,7 +61,7 @@ pyinfra/facts/iptables.py,sha256=ORRExNnPR5ysymDbkUjeQWt9mzjSI7su4uQfsu3FlJQ,350
60
61
  pyinfra/facts/launchd.py,sha256=Nl4EFbGwlXTj8GNWlS9UMZODHr63bve73xmMeqyf2gc,849
61
62
  pyinfra/facts/lxd.py,sha256=A2LQN5armY_nI6gV-m1j4l2J2_mMZnvVa1X2RntDnDk,548
62
63
  pyinfra/facts/mysql.py,sha256=ZVN6eN0Xetidvq_RAntSCzHgy4jmf8Enrm2mPXxO488,6247
63
- pyinfra/facts/npm.py,sha256=WzZHCfgWwY9WsPPzL0njlazwiBHI0gpvyZV93Nfu428,838
64
+ pyinfra/facts/npm.py,sha256=lbiLHoRMZ4POSjK_S5jbqp7ulrDqT-MIUhrI4uYV7BU,857
64
65
  pyinfra/facts/openrc.py,sha256=wB4fjzgWCgX6wexBm4jlWv4YH6b4z5_tAqQAO1qO9Kg,1614
65
66
  pyinfra/facts/opkg.py,sha256=yTPuzoUyDDn-o7H97lPtyPe-FOkYg5zqkVmfKt9HwY4,7404
66
67
  pyinfra/facts/pacman.py,sha256=SPI3jpzZbq_VnJ0z5q7A2Rw___EHVntOz6M19ErGpmQ,1324
@@ -74,7 +75,7 @@ pyinfra/facts/postgresql.py,sha256=4nusMVvGhtku86KX4O4vjSxh_MamxZy_kmTQvvy0GhE,2
74
75
  pyinfra/facts/rpm.py,sha256=ikuKYiUmjgvPA84qfE-gbq4Iv4AB5cvor1uKU6uHbXQ,2391
75
76
  pyinfra/facts/runit.py,sha256=qok1FTSshiNrN603vjYTKOeM-NIlxwLbwOp-vPbPySo,2131
76
77
  pyinfra/facts/selinux.py,sha256=umVGK_6Iuj4Xbw5vhhbpxdUA4Mzg2Fhy7QmaNflNrp8,4650
77
- pyinfra/facts/server.py,sha256=uafB7XYNT2eJB21XxNip_WrSyaRqe7uzFab5K4DCZqo,23446
78
+ pyinfra/facts/server.py,sha256=mfLHY8R6NxysPr9x_awB1lcxNplgnFFqrYdF30xrgms,23921
78
79
  pyinfra/facts/snap.py,sha256=2-c3z1qpOG7prmKJssLpOXmKo_wwdfROryro6gif2vo,2137
79
80
  pyinfra/facts/systemd.py,sha256=meHXURtnoxeJbmPzWFTwvhjQBZ2NlQCY8Tj-oHTG_dI,4320
80
81
  pyinfra/facts/sysvinit.py,sha256=q1OpHATFJxjLwatcnYRfpTR7_K2c29b4ppmZu-wgC-4,1589
@@ -90,60 +91,60 @@ pyinfra/facts/util/packaging.py,sha256=Bo7QxYO0Eyj4_i8G27aOWUD_Rfw741RhkEBm3dF5O
90
91
  pyinfra/facts/util/units.py,sha256=SNHCisxGwZedCOqO9tfOWJpZ5Stc0Wcg9mZcXoKBY0A,714
91
92
  pyinfra/facts/util/win_files.py,sha256=S_IQ5kJD6ZgkEcVHajgh7BIMolLV-1q1ghIcwAS-E1Q,2561
92
93
  pyinfra/operations/__init__.py,sha256=SOcW337KXIzD_LH-iJJfq14BQcCs5JzwswJ0PIzDgF4,357
93
- pyinfra/operations/apk.py,sha256=_0vOjbSiGx6EWv9rvTmQdGnRZQ_NA_Dyd3QW1cTzFgI,2111
94
- pyinfra/operations/apt.py,sha256=w8rb8_yH4gKrdhDLKfWcziC5XQRXfhcGqNUwVcqrXpk,14352
95
- pyinfra/operations/brew.py,sha256=aghLE4qyuhhRbt6fgSPV6_5fyWgTohA77Dc0gol19UU,5155
94
+ pyinfra/operations/apk.py,sha256=I1tYoPMN3OsIl_TJvmd_G1daqhiynOyK2-O2PxYHlUQ,2169
95
+ pyinfra/operations/apt.py,sha256=BrNbTlw1HRY5ZMOMFF0XCibeC8QsYM6bYlMRPuiUuNo,14767
96
+ pyinfra/operations/brew.py,sha256=o2T3siobQ_1CNDba7OInI2N60ReplRPELxgjEVDcCRg,5199
96
97
  pyinfra/operations/bsdinit.py,sha256=okQUQDr2H8Z-cAdfdbPJiuGujsHLuV5gpuMZ1UlICEM,1648
97
98
  pyinfra/operations/cargo.py,sha256=mXWd6pb0IR6kzJMmPHwXZN-VJ-B_y8AdOFlrRzDQOZI,1104
98
99
  pyinfra/operations/choco.py,sha256=nIj4bWhChOd5DkybpbD-oupaoODgS7lYx6Vrou5ksuc,1547
99
- pyinfra/operations/crontab.py,sha256=4jxNsgmJED7w9w2Rq6Irsa4wMXxH_yPDIjkFlcpXj7E,6552
100
- pyinfra/operations/dnf.py,sha256=3154Rer6dejVB1AK-CqyJhpMVn_djaSDJrVMs62GNcE,5599
101
- pyinfra/operations/docker.py,sha256=p8sG4BpcxSZwzKj4ystVgKmxmaF4GgDlnyJw1LeIsY4,12271
102
- pyinfra/operations/files.py,sha256=E4SQTZZSIihXUWuzp7a3zBp_5gy_Agx9Ag45zrh6pgs,65686
103
- pyinfra/operations/flatpak.py,sha256=QEJpzSXLyMQFk1XPAPHAfJVWcYWHnKA-tQr2hX6sB9o,2319
104
- pyinfra/operations/gem.py,sha256=2C85sOwIRMHGvmPg4uAlUVf6MokhiA7LLPqzdJRHsBg,1132
105
- pyinfra/operations/git.py,sha256=BqYQbG1VRw_XPtagbZHXitzss-U_LNGXRTwHE7L8P6M,13244
106
- pyinfra/operations/iptables.py,sha256=EVav144bK25F5KQcSivBWqIdffdskTcMcUuWaSK1RRE,9338
100
+ pyinfra/operations/crontab.py,sha256=L1U_fBvgXkbfbpzb6OzUBrrY-RuvvPlbW5FqDmAT8rI,6644
101
+ pyinfra/operations/dnf.py,sha256=wMFUoUB679bVydt01N7Sd7Cs16RhAaLca-zsmQU86rk,5727
102
+ pyinfra/operations/docker.py,sha256=J_vQwQmCZEuNASFrRjeudrr_ZrpbStdXybGGej4VMxc,14161
103
+ pyinfra/operations/files.py,sha256=jqkYIPn94z4MRqH1eU-ooOJzuQyle8wYiyDvFfXDMPI,68492
104
+ pyinfra/operations/flatpak.py,sha256=Eif5KZkWOVElKF4hL5xOyk_oZEOziHqyyDxGHZ1KPYk,2366
105
+ pyinfra/operations/gem.py,sha256=YtVUKVp1zYPAxy2t1ryw-vgucBVYJASOxhauLOvRj6U,1175
106
+ pyinfra/operations/git.py,sha256=IE41_MGOZ3nc7gtIzVssQDgi1eSoCbK07jm9EniFt54,13287
107
+ pyinfra/operations/iptables.py,sha256=ETdNPFkc5DyydBr7IncxYdR7DBAFQzE4X85AoGZGBZI,9386
107
108
  pyinfra/operations/launchd.py,sha256=6HWvqoQ74idV_NStOEmFXwu0dmTv7YDvFtsK8An2Lu4,1177
108
- pyinfra/operations/lxd.py,sha256=bKm9gsgZaruKYSL7OYFMiou-wGP4BzwIMWzjW4AZYrk,1742
109
- pyinfra/operations/mysql.py,sha256=mmTb8es8OQ9FB7dHpeD1aB3Px9p9Fp3WQEwMkWZ0-R4,19851
109
+ pyinfra/operations/lxd.py,sha256=eTsLOni3LAlq-f4dj4OJiHkLlhhMI7fW1N0M_7ekon4,1785
110
+ pyinfra/operations/mysql.py,sha256=kQmshmia7rNh9pZI3iGk95PG4T0TadChCiDYGy8jbuw,19896
110
111
  pyinfra/operations/npm.py,sha256=bUmfQsClZ2YcHiihiC7k5widIXIi6lbfx_32iyaAKfo,1499
111
112
  pyinfra/operations/openrc.py,sha256=AThQQO7u_pO0M-rQbxkX7EWDuR569smkoPVaQoRoFeE,1834
112
- pyinfra/operations/opkg.py,sha256=xBdmU07MWr-YBKX0EV0GuVrwMy6MOOf7wzYvD42sF_I,2607
113
- pyinfra/operations/pacman.py,sha256=QMjmsBiiw362nhZY0rEDVQL5A32MG3u7GcmX4q4PzfI,1702
114
- pyinfra/operations/pip.py,sha256=Gh1vmrkCNktYXO8bh0IMEqjqhV07J8FYJcLHETZhjcQ,5995
115
- pyinfra/operations/pipx.py,sha256=05AUm3nkkCfoxl0o4V5o9P_qb__0Yfv495dTBq7zkE4,2800
116
- pyinfra/operations/pkg.py,sha256=rORQBbKeb-6gS0LYu0a0VdiWcDZoovcUONCaf6KMdeQ,2298
117
- pyinfra/operations/pkgin.py,sha256=zhUyGzKjnUfGoyHbMoYMbeeMzcsiOUpBz1zIzppigJ0,1992
118
- pyinfra/operations/postgres.py,sha256=hD65hRb8s3h6zlG-9WwT4JsNEXuxJUCY8ZRX61qlvlI,13367
113
+ pyinfra/operations/opkg.py,sha256=ebv-SS0WaHp0dxKF2HnSY6m__vhQYNlh5zzbjgEzK9Q,2648
114
+ pyinfra/operations/pacman.py,sha256=e10D4XUwT0VTtZzBeB1tRomr9pEfun1VvNH1ia5eBhI,1748
115
+ pyinfra/operations/pip.py,sha256=uTfK36_vBNqCXsWMU4iypOLhnhKduJK6f0b9srlMEqg,6038
116
+ pyinfra/operations/pipx.py,sha256=oWcJXKogC43cKNsf625FU4ClIAV6KZA26o2cRoa3avQ,2844
117
+ pyinfra/operations/pkg.py,sha256=m5okKIXU1xIIcNQXQnFKXbL97ZcokmGn-hnruokE7is,2341
118
+ pyinfra/operations/pkgin.py,sha256=6bZyvdjYDqn-0a-r23O_122r1QSjHP8SkJiWZ_k231A,2037
119
+ pyinfra/operations/postgres.py,sha256=GlIn5aAVNOjs7cSyF3CFY6LO3dC0ZQQ0wAs_y0JJHis,13417
119
120
  pyinfra/operations/postgresql.py,sha256=agZjL2W4yxigk9ThIC0V_3wvmcWVdX308aJO24WkN6g,833
120
- pyinfra/operations/puppet.py,sha256=eDe8D9jQbHYQ4_r4-dmEZfMASKQvj36BR8z_h8aDfw8,861
121
- pyinfra/operations/python.py,sha256=u569cdPrPesrmzU09nwIPA3bk6TZ-Qv2QP0lJLcO_bw,2021
121
+ pyinfra/operations/puppet.py,sha256=e9vO6SQnkMoyVWjy3oP08GaXgyIPajoA2QJ4g4ib4-M,907
122
+ pyinfra/operations/python.py,sha256=5IXcywwhwITPRJAs8BEL5H5vSPvk_QFMbhF8Iuexp_s,2067
122
123
  pyinfra/operations/runit.py,sha256=-K0GhORE56J4Gjh7PCriSx9zZI7XJV-cIb-LnnSuKdY,5162
123
- pyinfra/operations/selinux.py,sha256=imZ4dbY4tl0GpBSkUgV983jbDDihWNs_OQkOBulT7FQ,5948
124
- pyinfra/operations/server.py,sha256=HsnNBZtjXq_SPIDacs1owzXbODBZya1idk5Cpp-32kc,31245
125
- pyinfra/operations/snap.py,sha256=a-QtNE4Dlsavqq425TUIwpEJu4oGw8UlLRkdTFyT1F8,3049
126
- pyinfra/operations/ssh.py,sha256=wocoaYDlOhhItItAVQCEfnVowTtkg3AP0hQ3mnpUnl0,5634
127
- pyinfra/operations/systemd.py,sha256=hPHTjASj6N_fRAzLr3DNHnxxIbiiTIIT9UStSxKDkTk,3984
128
- pyinfra/operations/sysvinit.py,sha256=WzzthkmWL46MNNY6LsBZ90e37yPj0w2QyUtEAlGBwqY,4078
124
+ pyinfra/operations/selinux.py,sha256=AZLLpYHBZbpLftUbigM7vjkjEajOQEQsLKc5y9OIpSQ,5995
125
+ pyinfra/operations/server.py,sha256=7CrmzWqTjQkync0y_Wdqt82VlmNn293XmoN720MSUWo,31291
126
+ pyinfra/operations/snap.py,sha256=vMCdH274ToI1Pi3un3qihZMS9UW9fXQD5PzLd7lT49E,3097
127
+ pyinfra/operations/ssh.py,sha256=8KdkzxevejX-PfQ6Lgt5c_sk8TutCxUOl9AbvwKUrlE,5677
128
+ pyinfra/operations/systemd.py,sha256=4geQSsuNNBC4F5S_gZBXLfH2UI0JgZZckQfNFJD2gDY,4031
129
+ pyinfra/operations/sysvinit.py,sha256=FMkjQ_jWxs9UR_QSRrogYMHT7lS4cmkDco-rU3waOWE,4119
129
130
  pyinfra/operations/upstart.py,sha256=pHb9RGnVhT14A_y6OezfOH-lmniKpiyJqpeoOJl0beE,1978
130
131
  pyinfra/operations/vzctl.py,sha256=2u2CDkuDjzHBRQ54HfyfLpLrsbT8U7_05EEjbbhKUiU,3110
131
- pyinfra/operations/xbps.py,sha256=ru3_srMBUyUXGzAsPo7WwoomfM0AeDglFv8CDqB33B0,1508
132
- pyinfra/operations/yum.py,sha256=Ig7AzQy1C7I8XM37lWbw0nI5lzFGMoX30P8FV8-V5uA,5600
133
- pyinfra/operations/zfs.py,sha256=FnhiXreqf5qJBusC31dwrQsEi0MvH4qxzjBayHKFcYY,5283
134
- pyinfra/operations/zypper.py,sha256=z1CWv2uwWBlCLIhHna7U5DojVoKZYoUYpezJ_FM_xK8,5555
132
+ pyinfra/operations/xbps.py,sha256=46t05vAdXj9r_VElMT0mxKTtIgeDKVK54wRB_2bSGCI,1552
133
+ pyinfra/operations/yum.py,sha256=Rls0ypn5_OaSGkpBHy---TLOw0fmvrecTQ8CuDR0VJE,5728
134
+ pyinfra/operations/zfs.py,sha256=1V-T04RN7Goxqj-wQzsqeXfCV5H1hEfys3l362lcRek,5326
135
+ pyinfra/operations/zypper.py,sha256=WEUmuRux7_TOdzeTZA_3I5qtqYPpr0N175qIKbYtI5M,5601
135
136
  pyinfra/operations/freebsd/__init__.py,sha256=PXsCLQG29VhimYw4uwwB2FcDt6CrofOtD3LTHC-ik-8,362
136
137
  pyinfra/operations/freebsd/freebsd_update.py,sha256=lH0wB_i6DkaXQnDPjsDq8ocO1hYHL0v_a1go4exYcR8,1864
137
138
  pyinfra/operations/freebsd/pkg.py,sha256=3AyfI0-_9F4ho47KqZsOMQocwNtTF2q9g0i6TffJVak,4413
138
139
  pyinfra/operations/freebsd/service.py,sha256=1f7nTHELnhs3HBSrMFsmopVgYFMIwB8Se88yneRQ8Rw,3198
139
140
  pyinfra/operations/freebsd/sysrc.py,sha256=eg7u_JsCge_uKq3Ysc_mohUc6qgJrOZStp_B_l2Hav4,2330
140
141
  pyinfra/operations/util/__init__.py,sha256=ZAHjeCXtLo0TIOSfZ9h0Sh5IXXRCspfHs3RR1l8tQCE,366
141
- pyinfra/operations/util/docker.py,sha256=F7YmWIRSDyFaosf9q81nnwHhhIG7ktXOkM4VVOiUXTs,6931
142
+ pyinfra/operations/util/docker.py,sha256=OCjlyxjuNP6Ki8zU6-cQkhgM-FgKsylD-dEhR7JNc8Y,11920
142
143
  pyinfra/operations/util/files.py,sha256=PFJDccNTwXK4tIoFB8ycRj7yD1x7LpSflBy7mPQtJCg,7148
143
- pyinfra/operations/util/packaging.py,sha256=s5lP7lRORirQVDwGRARAatUZvz5lv_WpSSP-qq__2JQ,12034
144
+ pyinfra/operations/util/packaging.py,sha256=RXZgUlWqEBtArK7wJfXE2Ndvl_aP0YjjksxxCnPpexk,12086
144
145
  pyinfra/operations/util/service.py,sha256=kJd1zj4-sAaGIp5Ts7yAJznogWaGr8oQTztwenLAr7Y,1309
145
146
  pyinfra_cli/__init__.py,sha256=G0X7tNdqT45uWuK3aHIKxMdDeCgJ7zHo6vbxoG6zy_8,284
146
- pyinfra_cli/cli.py,sha256=Bpt-mRNmvz0sNNhMAO5J3m0Kg-6M8tkJNMGY4C01ARc,20938
147
+ pyinfra_cli/cli.py,sha256=vlD9YyJaSy0mZu2J6pvDvBxka6cu6pRCvDj_ZZUDqXY,21285
147
148
  pyinfra_cli/commands.py,sha256=J-mCJYvDebJ8M7o3HreB2zToa871-xO6_KjVhPLeHho,1832
148
149
  pyinfra_cli/exceptions.py,sha256=RRaOprL7SmVv--FLy4x7fxeTitx9wYI0Y3_h01LfhJA,4901
149
150
  pyinfra_cli/inventory.py,sha256=JYSixJZKY8GNWlOxh-nGDsAknCdaAktlWAmdg13kvNk,11771
@@ -152,8 +153,8 @@ pyinfra_cli/main.py,sha256=1CR3IS-O6BkAzkn7UW6pdKktTmN4Qnt0_jPdkhueRM8,936
152
153
  pyinfra_cli/prints.py,sha256=1h6vgKVRKUxcGz_HdyEEDUvkp-lgiiVGwx3hc9rw24A,10434
153
154
  pyinfra_cli/util.py,sha256=8KKW5LTHX4ebbwbHqMvLqwjZ11mOHI-xIYn-cZCWltg,6722
154
155
  pyinfra_cli/virtualenv.py,sha256=wRNxOPcUkbD_Pzuj-Lnrz1KxGmsLlb2ObmCTFrdD-S8,2474
155
- pyinfra-3.5.1.dist-info/METADATA,sha256=vlQV0UOJp4UabXoiD6rlQU6HV1oJ76tj3jklwU3cBPk,5740
156
- pyinfra-3.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
157
- pyinfra-3.5.1.dist-info/entry_points.txt,sha256=b1nLI6oVRvkeQDS00xcYGdGl4XVR_3tMbKs6T58-NW4,507
158
- pyinfra-3.5.1.dist-info/licenses/LICENSE.md,sha256=BzCnRYLJv0yb-FJuEd_XOrrQSOEQKzIVo0yHT8taNnM,1076
159
- pyinfra-3.5.1.dist-info/RECORD,,
156
+ pyinfra-3.6.dist-info/METADATA,sha256=8nSBGBw02ni-nBp6iJsQ6hzoHPoytQcD8jKptRxm4Jg,5771
157
+ pyinfra-3.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
158
+ pyinfra-3.6.dist-info/entry_points.txt,sha256=b1nLI6oVRvkeQDS00xcYGdGl4XVR_3tMbKs6T58-NW4,507
159
+ pyinfra-3.6.dist-info/licenses/LICENSE.md,sha256=BzCnRYLJv0yb-FJuEd_XOrrQSOEQKzIVo0yHT8taNnM,1076
160
+ pyinfra-3.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pyinfra_cli/cli.py CHANGED
@@ -2,6 +2,7 @@ import logging
2
2
  import sys
3
3
  import warnings
4
4
  from fnmatch import fnmatch
5
+ from getpass import getpass
5
6
  from os import chdir as os_chdir, getcwd, path
6
7
  from typing import Iterable, List, Tuple, Union
7
8
 
@@ -116,6 +117,12 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
116
117
  help="Whether to execute operations with sudo.",
117
118
  )
118
119
  @click.option("--sudo-user", help="Which user to sudo when sudoing.")
120
+ @click.option(
121
+ "--same-sudo-password",
122
+ is_flag=True,
123
+ default=False,
124
+ help="All hosts have the same sudo password, so ask only once.",
125
+ )
119
126
  @click.option(
120
127
  "--use-sudo-password",
121
128
  is_flag=True,
@@ -274,6 +281,7 @@ def _main(
274
281
  ssh_key,
275
282
  ssh_key_password: str,
276
283
  ssh_password: str,
284
+ same_sudo_password: bool,
277
285
  shell_executable,
278
286
  sudo: bool,
279
287
  sudo_user: str,
@@ -326,6 +334,7 @@ def _main(
326
334
  sudo,
327
335
  sudo_user,
328
336
  use_sudo_password,
337
+ same_sudo_password,
329
338
  su_user,
330
339
  parallel,
331
340
  shell_executable,
@@ -568,6 +577,7 @@ def _set_config(
568
577
  sudo,
569
578
  sudo_user,
570
579
  use_sudo_password,
580
+ same_sudo_password,
571
581
  su_user,
572
582
  parallel,
573
583
  shell_executable,
@@ -598,6 +608,9 @@ def _set_config(
598
608
  if use_sudo_password:
599
609
  config.USE_SUDO_PASSWORD = use_sudo_password
600
610
 
611
+ if same_sudo_password:
612
+ config.SUDO_PASSWORD = getpass("sudo password: ")
613
+
601
614
  if su_user:
602
615
  config.SU_USER = su_user
603
616