pyinfra 3.1.1__py2.py3-none-any.whl → 3.3__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.
Files changed (104) hide show
  1. pyinfra/api/arguments.py +9 -2
  2. pyinfra/api/arguments_typed.py +4 -5
  3. pyinfra/api/command.py +22 -3
  4. pyinfra/api/config.py +5 -2
  5. pyinfra/api/deploy.py +4 -2
  6. pyinfra/api/facts.py +3 -0
  7. pyinfra/api/host.py +15 -7
  8. pyinfra/api/operation.py +2 -1
  9. pyinfra/api/state.py +1 -1
  10. pyinfra/connectors/base.py +34 -8
  11. pyinfra/connectors/chroot.py +7 -2
  12. pyinfra/connectors/docker.py +24 -8
  13. pyinfra/connectors/dockerssh.py +7 -2
  14. pyinfra/connectors/local.py +7 -2
  15. pyinfra/connectors/ssh.py +9 -2
  16. pyinfra/connectors/sshuserclient/client.py +42 -14
  17. pyinfra/connectors/sshuserclient/config.py +2 -0
  18. pyinfra/connectors/terraform.py +1 -1
  19. pyinfra/connectors/util.py +13 -9
  20. pyinfra/context.py +9 -2
  21. pyinfra/facts/apk.py +8 -1
  22. pyinfra/facts/apt.py +68 -0
  23. pyinfra/facts/brew.py +13 -0
  24. pyinfra/facts/bsdinit.py +3 -0
  25. pyinfra/facts/cargo.py +5 -0
  26. pyinfra/facts/choco.py +6 -0
  27. pyinfra/facts/crontab.py +195 -0
  28. pyinfra/facts/deb.py +10 -0
  29. pyinfra/facts/dnf.py +5 -0
  30. pyinfra/facts/docker.py +16 -0
  31. pyinfra/facts/efibootmgr.py +113 -0
  32. pyinfra/facts/files.py +112 -7
  33. pyinfra/facts/flatpak.py +7 -0
  34. pyinfra/facts/freebsd.py +75 -0
  35. pyinfra/facts/gem.py +5 -0
  36. pyinfra/facts/git.py +12 -2
  37. pyinfra/facts/gpg.py +7 -0
  38. pyinfra/facts/hardware.py +13 -0
  39. pyinfra/facts/iptables.py +9 -1
  40. pyinfra/facts/launchd.py +5 -0
  41. pyinfra/facts/lxd.py +5 -0
  42. pyinfra/facts/mysql.py +9 -2
  43. pyinfra/facts/npm.py +5 -0
  44. pyinfra/facts/openrc.py +8 -0
  45. pyinfra/facts/opkg.py +245 -0
  46. pyinfra/facts/pacman.py +9 -1
  47. pyinfra/facts/pip.py +5 -0
  48. pyinfra/facts/pipx.py +82 -0
  49. pyinfra/facts/pkg.py +4 -0
  50. pyinfra/facts/pkgin.py +5 -0
  51. pyinfra/facts/podman.py +54 -0
  52. pyinfra/facts/postgres.py +10 -2
  53. pyinfra/facts/rpm.py +11 -0
  54. pyinfra/facts/runit.py +7 -0
  55. pyinfra/facts/selinux.py +16 -0
  56. pyinfra/facts/server.py +87 -79
  57. pyinfra/facts/snap.py +7 -0
  58. pyinfra/facts/systemd.py +5 -0
  59. pyinfra/facts/sysvinit.py +4 -0
  60. pyinfra/facts/upstart.py +5 -0
  61. pyinfra/facts/util/__init__.py +4 -1
  62. pyinfra/facts/util/units.py +30 -0
  63. pyinfra/facts/vzctl.py +5 -0
  64. pyinfra/facts/xbps.py +6 -1
  65. pyinfra/facts/yum.py +5 -0
  66. pyinfra/facts/zfs.py +41 -21
  67. pyinfra/facts/zypper.py +5 -0
  68. pyinfra/local.py +3 -2
  69. pyinfra/operations/apt.py +36 -22
  70. pyinfra/operations/crontab.py +189 -0
  71. pyinfra/operations/docker.py +61 -56
  72. pyinfra/operations/files.py +65 -1
  73. pyinfra/operations/freebsd/__init__.py +12 -0
  74. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  75. pyinfra/operations/freebsd/pkg.py +219 -0
  76. pyinfra/operations/freebsd/service.py +116 -0
  77. pyinfra/operations/freebsd/sysrc.py +92 -0
  78. pyinfra/operations/git.py +23 -7
  79. pyinfra/operations/opkg.py +88 -0
  80. pyinfra/operations/pip.py +3 -2
  81. pyinfra/operations/pipx.py +90 -0
  82. pyinfra/operations/postgres.py +114 -27
  83. pyinfra/operations/runit.py +2 -0
  84. pyinfra/operations/server.py +9 -181
  85. pyinfra/operations/util/docker.py +44 -22
  86. pyinfra/operations/zfs.py +3 -3
  87. {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/LICENSE.md +1 -1
  88. {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/METADATA +25 -25
  89. pyinfra-3.3.dist-info/RECORD +187 -0
  90. pyinfra_cli/exceptions.py +5 -0
  91. pyinfra_cli/inventory.py +26 -9
  92. pyinfra_cli/log.py +3 -0
  93. pyinfra_cli/main.py +9 -8
  94. pyinfra_cli/prints.py +19 -4
  95. pyinfra_cli/util.py +3 -0
  96. pyinfra_cli/virtualenv.py +1 -1
  97. tests/test_cli/test_cli_deploy.py +15 -13
  98. tests/test_cli/test_cli_inventory.py +53 -0
  99. tests/test_connectors/test_ssh.py +302 -182
  100. tests/test_connectors/test_sshuserclient.py +68 -1
  101. pyinfra-3.1.1.dist-info/RECORD +0 -172
  102. {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/WHEEL +0 -0
  103. {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/entry_points.txt +0 -0
  104. {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/top_level.txt +0 -0
pyinfra/facts/vzctl.py CHANGED
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
 
5
+ from typing_extensions import override
6
+
5
7
  from pyinfra.api import FactBase
6
8
 
7
9
 
@@ -20,14 +22,17 @@ class OpenvzContainers(FactBase):
20
22
  }
21
23
  """
22
24
 
25
+ @override
23
26
  def command(self) -> str:
24
27
  return "vzlist -a -j"
25
28
 
29
+ @override
26
30
  def requires_command(self) -> str:
27
31
  return "vzlist"
28
32
 
29
33
  default = dict
30
34
 
35
+ @override
31
36
  def process(self, output):
32
37
  combined_json = "".join(output)
33
38
  vz_data = json.loads(combined_json)
pyinfra/facts/xbps.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing_extensions import override
4
+
3
5
  from pyinfra.api import FactBase
4
6
 
5
7
  from .util.packaging import parse_packages
@@ -16,15 +18,18 @@ class XbpsPackages(FactBase):
16
18
  }
17
19
  """
18
20
 
21
+ @override
19
22
  def requires_command(self) -> str:
20
23
  return "xbps-query"
21
24
 
22
25
  default = dict
23
26
 
24
- regex = r"^.. ([a-zA-Z0-9_\-\+]+)\-([0-9a-z_\.]+)"
27
+ regex = r"^.. ([a-zA-Z0-9_\-\+\.]+)\-([0-9a-z\.]+_[0-9]+)"
25
28
 
29
+ @override
26
30
  def command(self):
27
31
  return "xbps-query -l"
28
32
 
33
+ @override
29
34
  def process(self, output):
30
35
  return parse_packages(self.regex, output)
pyinfra/facts/yum.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing_extensions import override
4
+
3
5
  from pyinfra.api import FactBase
4
6
 
5
7
  from .util import make_cat_files_command
@@ -23,16 +25,19 @@ class YumRepositories(FactBase):
23
25
  ]
24
26
  """
25
27
 
28
+ @override
26
29
  def command(self) -> str:
27
30
  return make_cat_files_command(
28
31
  "/etc/yum.conf",
29
32
  "/etc/yum.repos.d/*.repo",
30
33
  )
31
34
 
35
+ @override
32
36
  def requires_command(self) -> str:
33
37
  return "yum"
34
38
 
35
39
  default = list
36
40
 
41
+ @override
37
42
  def process(self, output):
38
43
  return parse_yum_repositories(output)
pyinfra/facts/zfs.py CHANGED
@@ -1,7 +1,9 @@
1
1
  """
2
- Manage ZFS filesystems.
2
+ Gather information about ZFS filesystems.
3
3
  """
4
4
 
5
+ from typing_extensions import override
6
+
5
7
  from pyinfra.api import FactBase, ShortFactBase
6
8
 
7
9
 
@@ -15,43 +17,61 @@ def _process_zfs_props_table(output):
15
17
  return datasets
16
18
 
17
19
 
18
- class Pools(FactBase):
19
- def command(self):
20
+ class ZfsPools(FactBase):
21
+ @override
22
+ def command(self) -> str:
20
23
  return "zpool get -H all"
21
24
 
22
- @staticmethod
23
- def process(output):
25
+ @override
26
+ def requires_command(self) -> str:
27
+ return "zpool"
28
+
29
+ @override
30
+ def process(self, output):
24
31
  return _process_zfs_props_table(output)
25
32
 
26
33
 
27
- class Datasets(FactBase):
28
- def command(self):
34
+ class ZfsDatasets(FactBase):
35
+ @override
36
+ def command(self) -> str:
29
37
  return "zfs get -H all"
30
38
 
31
- @staticmethod
32
- def process(output):
39
+ @override
40
+ def requires_command(self) -> str:
41
+ return "zfs"
42
+
43
+ @override
44
+ def process(self, output):
33
45
  return _process_zfs_props_table(output)
34
46
 
35
47
 
36
- class Filesystems(ShortFactBase):
37
- fact = Datasets
48
+ class ZfsFilesystems(ShortFactBase):
49
+ fact = ZfsDatasets
38
50
 
39
- @staticmethod
40
- def process_data(data):
51
+ @override
52
+ def process_data(self, data):
41
53
  return {name: props for name, props in data.items() if props.get("type") == "filesystem"}
42
54
 
43
55
 
44
- class Snapshots(ShortFactBase):
45
- fact = Datasets
56
+ class ZfsSnapshots(ShortFactBase):
57
+ fact = ZfsDatasets
46
58
 
47
- @staticmethod
48
- def process_data(data):
59
+ @override
60
+ def process_data(self, data):
49
61
  return {name: props for name, props in data.items() if props.get("type") == "snapshot"}
50
62
 
51
63
 
52
- class Volumes(ShortFactBase):
53
- fact = Datasets
64
+ class ZfsVolumes(ShortFactBase):
65
+ fact = ZfsDatasets
54
66
 
55
- @staticmethod
56
- def process_data(data):
67
+ @override
68
+ def process_data(self, data):
57
69
  return {name: props for name, props in data.items() if props.get("type") == "volume"}
70
+
71
+
72
+ # TODO: remove these in v4! Or flip the convention and remove all the other fact prefixes!
73
+ Pools = ZfsPools
74
+ Datasets = ZfsDatasets
75
+ Filesystems = ZfsFilesystems
76
+ Snapshots = ZfsSnapshots
77
+ Volumes = ZfsVolumes
pyinfra/facts/zypper.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing_extensions import override
4
+
3
5
  from pyinfra.api import FactBase
4
6
 
5
7
  from .util import make_cat_files_command
@@ -23,15 +25,18 @@ class ZypperRepositories(FactBase):
23
25
  ]
24
26
  """
25
27
 
28
+ @override
26
29
  def command(self) -> str:
27
30
  return make_cat_files_command(
28
31
  "/etc/zypp/repos.d/*.repo",
29
32
  )
30
33
 
34
+ @override
31
35
  def requires_command(self) -> str:
32
36
  return "zypper"
33
37
 
34
38
  default = list
35
39
 
40
+ @override
36
41
  def process(self, output):
37
42
  return parse_zypper_repositories(output)
pyinfra/local.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from os import path
2
+ from typing import Optional
2
3
 
3
4
  import click
4
5
 
@@ -10,7 +11,7 @@ from pyinfra.connectors.util import run_local_process
10
11
  from pyinfra.context import ctx_state
11
12
 
12
13
 
13
- def include(filename: str):
14
+ def include(filename: str, data: Optional[dict] = None):
14
15
  """
15
16
  Executes a local python file within the ``pyinfra.state.cwd``
16
17
  directory.
@@ -33,7 +34,7 @@ def include(filename: str):
33
34
 
34
35
  from pyinfra_cli.util import exec_file
35
36
 
36
- with host.deploy(path.relpath(filename, state.cwd), None, None, in_deploy=False):
37
+ with host.deploy(path.relpath(filename, state.cwd), None, data, in_deploy=False):
37
38
  exec_file(filename)
38
39
 
39
40
  # One potential solution to the above is to add local as an actual
pyinfra/operations/apt.py CHANGED
@@ -9,7 +9,13 @@ from urllib.parse import urlparse
9
9
 
10
10
  from pyinfra import host
11
11
  from pyinfra.api import OperationError, operation
12
- from pyinfra.facts.apt import AptKeys, AptSources, parse_apt_repo
12
+ from pyinfra.facts.apt import (
13
+ AptKeys,
14
+ AptSources,
15
+ SimulateOperationWillChange,
16
+ noninteractive_apt,
17
+ parse_apt_repo,
18
+ )
13
19
  from pyinfra.facts.deb import DebPackage, DebPackages
14
20
  from pyinfra.facts.files import File
15
21
  from pyinfra.facts.gpg import GpgKey
@@ -21,21 +27,22 @@ from .util.packaging import ensure_packages
21
27
  APT_UPDATE_FILENAME = "/var/lib/apt/periodic/update-success-stamp"
22
28
 
23
29
 
24
- def noninteractive_apt(command: str, force=False):
25
- args = ["DEBIAN_FRONTEND=noninteractive apt-get -y"]
26
-
27
- if force:
28
- args.append("--force-yes")
29
-
30
- args.extend(
31
- (
32
- '-o Dpkg::Options::="--force-confdef"',
33
- '-o Dpkg::Options::="--force-confold"',
34
- command,
35
- ),
36
- )
37
-
38
- return " ".join(args)
30
+ def _simulate_then_perform(command: str):
31
+ changes = host.get_fact(SimulateOperationWillChange, command)
32
+
33
+ if not changes:
34
+ # Simulating apt-get command failed, so the actual
35
+ # operation will probably fail too:
36
+ yield noninteractive_apt(command)
37
+ elif (
38
+ changes["upgraded"] == 0
39
+ and changes["newly_installed"] == 0
40
+ and changes["removed"] == 0
41
+ and changes["not_upgraded"] == 0
42
+ ):
43
+ host.noop(f"{command} skipped, no changes would be performed")
44
+ else:
45
+ yield noninteractive_apt(command)
39
46
 
40
47
 
41
48
  @operation()
@@ -321,12 +328,12 @@ def update(cache_time: int | None = None):
321
328
  _update = update # noqa: E305
322
329
 
323
330
 
324
- @operation(is_idempotent=False)
331
+ @operation()
325
332
  def upgrade(auto_remove: bool = False):
326
333
  """
327
334
  Upgrades all apt packages.
328
335
 
329
- + autoremove: removes transitive dependencies that are no longer needed.
336
+ + auto_remove: removes transitive dependencies that are no longer needed.
330
337
 
331
338
  **Example:**
332
339
 
@@ -349,17 +356,19 @@ def upgrade(auto_remove: bool = False):
349
356
  if auto_remove:
350
357
  command.append("--autoremove")
351
358
 
352
- yield noninteractive_apt(" ".join(command))
359
+ yield from _simulate_then_perform(" ".join(command))
353
360
 
354
361
 
355
362
  _upgrade = upgrade # noqa: E305 (for use below where update is a kwarg)
356
363
 
357
364
 
358
- @operation(is_idempotent=False)
359
- def dist_upgrade():
365
+ @operation()
366
+ def dist_upgrade(auto_remove: bool = False):
360
367
  """
361
368
  Updates all apt packages, employing dist-upgrade.
362
369
 
370
+ + auto_remove: removes transitive dependencies that are no longer needed.
371
+
363
372
  **Example:**
364
373
 
365
374
  .. code:: python
@@ -369,7 +378,12 @@ def dist_upgrade():
369
378
  )
370
379
  """
371
380
 
372
- yield noninteractive_apt("dist-upgrade")
381
+ command = ["dist-upgrade"]
382
+
383
+ if auto_remove:
384
+ command.append("--autoremove")
385
+
386
+ yield from _simulate_then_perform(" ".join(command))
373
387
 
374
388
 
375
389
  @operation()
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
5
+ from pyinfra import host
6
+ from pyinfra.api import StringCommand, operation
7
+ from pyinfra.api.util import try_int
8
+ from pyinfra.facts.crontab import Crontab, CrontabFile
9
+ from pyinfra.operations.util.files import sed_replace
10
+
11
+
12
+ @operation()
13
+ def crontab(
14
+ command: str,
15
+ present=True,
16
+ user: str | None = None,
17
+ cron_name: str | None = None,
18
+ minute="*",
19
+ hour="*",
20
+ month="*",
21
+ day_of_week="*",
22
+ day_of_month="*",
23
+ special_time: str | None = None,
24
+ interpolate_variables=False,
25
+ ):
26
+ """
27
+ Add/remove/update crontab entries.
28
+
29
+ + command: the command for the cron
30
+ + present: whether this cron command should exist
31
+ + user: the user whose crontab to manage
32
+ + cron_name: name the cronjob so future changes to the command will overwrite
33
+ + modify_cron_name: modify the cron name
34
+ + minute: which minutes to execute the cron
35
+ + hour: which hours to execute the cron
36
+ + month: which months to execute the cron
37
+ + day_of_week: which day of the week to execute the cron
38
+ + day_of_month: which day of the month to execute the cron
39
+ + special_time: cron "nickname" time (@reboot, @daily, etc), overrides others
40
+ + interpolate_variables: whether to interpolate variables in ``command``
41
+
42
+ Cron commands:
43
+ Unless ``name`` is specified the command is used to identify crontab entries.
44
+ This means commands must be unique within a given users crontab. If you require
45
+ multiple identical commands, provide a different name argument for each.
46
+
47
+ Special times:
48
+ When provided, ``special_time`` will be used instead of any values passed in
49
+ for ``minute``/``hour``/``month``/``day_of_week``/``day_of_month``.
50
+
51
+ **Example:**
52
+
53
+ .. code:: python
54
+
55
+ # simple example for a crontab
56
+ crontab.crontab(
57
+ name="Backup /etc weekly",
58
+ command="/bin/tar cf /tmp/etc_bup.tar /etc",
59
+ name="backup_etc",
60
+ day_of_week=0,
61
+ hour=1,
62
+ minute=0,
63
+ )
64
+ """
65
+
66
+ def comma_sep(value):
67
+ if isinstance(value, (list, tuple)):
68
+ return ",".join("{0}".format(v) for v in value)
69
+ return value
70
+
71
+ minute = comma_sep(minute)
72
+ hour = comma_sep(hour)
73
+ month = comma_sep(month)
74
+ day_of_week = comma_sep(day_of_week)
75
+ day_of_month = comma_sep(day_of_month)
76
+
77
+ ctb0: CrontabFile | dict = host.get_fact(Crontab, user=user)
78
+ # facts from test are in dict
79
+ if isinstance(ctb0, dict):
80
+ ctb = CrontabFile(ctb0)
81
+ else:
82
+ ctb = ctb0
83
+ name_comment = "# pyinfra-name={0}".format(cron_name)
84
+
85
+ existing_crontab = ctb.get_command(command=command, name=cron_name)
86
+ existing_crontab_command = existing_crontab["command"] if existing_crontab else command
87
+ existing_crontab_match = existing_crontab["command"] if existing_crontab else command
88
+
89
+ exists = existing_crontab is not None
90
+ exists_name = existing_crontab is not None and name_comment in existing_crontab.get(
91
+ "comments", ""
92
+ )
93
+
94
+ edit_commands: list[str | StringCommand] = []
95
+ temp_filename = host.get_temp_filename()
96
+
97
+ if special_time:
98
+ new_crontab_line = "{0} {1}".format(special_time, command)
99
+ else:
100
+ new_crontab_line = "{minute} {hour} {day_of_month} {month} {day_of_week} {command}".format(
101
+ minute=minute,
102
+ hour=hour,
103
+ day_of_month=day_of_month,
104
+ month=month,
105
+ day_of_week=day_of_week,
106
+ command=command,
107
+ )
108
+
109
+ existing_crontab_match = ".*{0}.*".format(existing_crontab_match)
110
+
111
+ # Don't want the cron and it does exist? Remove the line
112
+ if not present and exists:
113
+ edit_commands.append(
114
+ sed_replace(
115
+ temp_filename,
116
+ existing_crontab_match,
117
+ "",
118
+ interpolate_variables=interpolate_variables,
119
+ ),
120
+ )
121
+
122
+ # Want the cron but it doesn't exist? Append the line
123
+ elif present and not exists:
124
+ print("present", present, "exists", exists)
125
+ if ctb: # append a blank line if cron entries already exist
126
+ edit_commands.append("echo '' >> {0}".format(temp_filename))
127
+ if cron_name:
128
+ edit_commands.append(
129
+ "echo {0} >> {1}".format(
130
+ shlex.quote(name_comment),
131
+ temp_filename,
132
+ ),
133
+ )
134
+
135
+ edit_commands.append(
136
+ "echo {0} >> {1}".format(
137
+ shlex.quote(new_crontab_line),
138
+ temp_filename,
139
+ ),
140
+ )
141
+
142
+ # We have the cron and it exists, do it's details? If not, replace the line
143
+ elif present and exists:
144
+ assert existing_crontab is not None
145
+ if any(
146
+ (
147
+ exists_name != (cron_name is not None),
148
+ special_time != existing_crontab.get("special_time"),
149
+ try_int(minute) != existing_crontab.get("minute"),
150
+ try_int(hour) != existing_crontab.get("hour"),
151
+ try_int(month) != existing_crontab.get("month"),
152
+ try_int(day_of_week) != existing_crontab.get("day_of_week"),
153
+ try_int(day_of_month) != existing_crontab.get("day_of_month"),
154
+ existing_crontab_command != command,
155
+ ),
156
+ ):
157
+ if not exists_name and cron_name:
158
+ new_crontab_line = f"{name_comment}\n{new_crontab_line}"
159
+ edit_commands.append(
160
+ sed_replace(
161
+ temp_filename,
162
+ existing_crontab_match,
163
+ new_crontab_line,
164
+ interpolate_variables=interpolate_variables,
165
+ ),
166
+ )
167
+
168
+ if edit_commands:
169
+ crontab_args = []
170
+ if user:
171
+ crontab_args.append("-u {0}".format(user))
172
+
173
+ # List the crontab into a temporary file if it exists
174
+ if ctb:
175
+ yield "crontab -l {0} > {1}".format(" ".join(crontab_args), temp_filename)
176
+
177
+ # Now yield any edits
178
+ for edit_command in edit_commands:
179
+ yield edit_command
180
+
181
+ # Finally, use the tempfile to write a new crontab
182
+ yield "crontab {0} {1}".format(" ".join(crontab_args), temp_filename)
183
+ else:
184
+ host.noop(
185
+ "crontab {0} {1}".format(
186
+ command,
187
+ "exists" if present else "does not exist",
188
+ ),
189
+ )