pyinfra 0.11.dev3__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 (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/context.py ADDED
@@ -0,0 +1,145 @@
1
+ """
2
+ The `ContextObject` and `ContextManager` provide context specific variables that
3
+ are imported and used throughout pyinfra and end user deploy code (CLI mode).
4
+
5
+ These variables always represent the current executing pyinfra context.
6
+ """
7
+
8
+ from contextlib import contextmanager
9
+ from types import ModuleType
10
+ from typing import TYPE_CHECKING
11
+
12
+ from gevent.local import local
13
+ from typing_extensions import override
14
+
15
+ if TYPE_CHECKING:
16
+ from pyinfra.api.config import Config
17
+ from pyinfra.api.host import Host
18
+ from pyinfra.api.inventory import Inventory
19
+ from pyinfra.api.state import State
20
+
21
+
22
+ class container:
23
+ module = None
24
+
25
+
26
+ class ContextObject:
27
+ _container_cls = container
28
+ _base_cls: ModuleType
29
+
30
+ def __init__(self) -> None:
31
+ self._container = self._container_cls()
32
+ self._container.module = None
33
+
34
+ def _get_module(self):
35
+ return self._container.module
36
+
37
+ @override
38
+ def __repr__(self):
39
+ return "ContextObject({0}):{1}".format(
40
+ self._base_cls.__name__,
41
+ repr(self._get_module()),
42
+ )
43
+
44
+ @override
45
+ def __str__(self):
46
+ return str(self._get_module())
47
+
48
+ @override
49
+ def __dir__(self):
50
+ return dir(self._base_cls)
51
+
52
+ def __getattr__(self, key):
53
+ if self._get_module() is None:
54
+ return getattr(self._base_cls, key)
55
+ return getattr(self._get_module(), key)
56
+
57
+ @override
58
+ def __setattr__(self, key, value):
59
+ if key in ("_container", "_base_cls"):
60
+ return super().__setattr__(key, value)
61
+
62
+ mod = self._get_module()
63
+ if mod is None:
64
+ raise TypeError("Cannot assign to context base module")
65
+ return setattr(mod, key, value)
66
+
67
+ def __iter__(self):
68
+ mod = self._get_module()
69
+ if mod is None:
70
+ raise ValueError("Context not set")
71
+ return iter(mod)
72
+
73
+ def __len__(self):
74
+ mod = self._get_module()
75
+ if mod is None:
76
+ raise ValueError("Context not set")
77
+ return len(mod)
78
+
79
+ @override
80
+ def __eq__(self, other):
81
+ return self._get_module() == other
82
+
83
+ @override
84
+ def __hash__(self):
85
+ return hash(self._get_module())
86
+
87
+
88
+ class LocalContextObject(ContextObject):
89
+ _container_cls = local
90
+
91
+
92
+ class ContextManager:
93
+ def __init__(self, key, context_cls):
94
+ self.context = context_cls()
95
+
96
+ def get(self):
97
+ return getattr(self.context._container, "module", None)
98
+
99
+ def set(self, module):
100
+ self.context._container.module = module
101
+
102
+ def set_base(self, module):
103
+ self.context._base_cls = module
104
+
105
+ def reset(self) -> None:
106
+ self.context._container.module = None
107
+
108
+ def isset(self):
109
+ return self.get() is not None
110
+
111
+ @contextmanager
112
+ def use(self, module):
113
+ old_module = self.get()
114
+ if old_module is module:
115
+ yield # if we're double-setting, nothing to do
116
+ return
117
+ self.set(module)
118
+ yield
119
+ self.set(old_module)
120
+
121
+
122
+ ctx_state = ContextManager("state", ContextObject)
123
+ state: "State" = ctx_state.context
124
+
125
+ ctx_inventory = ContextManager("inventory", ContextObject)
126
+ inventory: "Inventory" = ctx_inventory.context
127
+
128
+ # Config can be modified mid-deploy, so we use a local object here which
129
+ # is based on a copy of the state config.
130
+ ctx_config = ContextManager("config", LocalContextObject)
131
+ config: "Config" = ctx_config.context
132
+
133
+ # Hosts are prepared in parallel each in a greenlet, so we use a local to
134
+ # point at different host objects in each greenlet.
135
+ ctx_host = ContextManager("host", LocalContextObject)
136
+ host: "Host" = ctx_host.context
137
+
138
+
139
+ def init_base_classes() -> None:
140
+ from pyinfra.api import Config, Host, Inventory, State
141
+
142
+ ctx_config.set_base(Config)
143
+ ctx_inventory.set_base(Inventory)
144
+ ctx_host.set_base(Host)
145
+ ctx_state.set_base(State)
pyinfra/facts/__init__.py CHANGED
@@ -1,11 +1,12 @@
1
+ # This file only exists to support:
2
+ # from pyinfra import facts
3
+ # facts.X.Y
4
+
1
5
  from glob import glob
2
6
  from os import path
3
7
 
4
-
5
- module_filenames = glob(path.join(path.dirname(__file__), '*.py'))
8
+ module_filenames = glob(path.join(path.dirname(__file__), "*.py"))
6
9
  module_names = [path.basename(name)[:-3] for name in module_filenames]
7
- __all__ = [name for name in module_names if name != '__init__']
8
-
10
+ __all__ = [name for name in module_names if name != "__init__"]
9
11
 
10
- # Actually triggers __all__ imports to builds facts index
11
- from . import * # noqa
12
+ from . import * # noqa
pyinfra/facts/apk.py CHANGED
@@ -1,22 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
1
5
  from pyinfra.api import FactBase
2
6
 
3
7
  from .util.packaging import parse_packages
4
8
 
5
- APK_REGEX = r'^([a-zA-Z\-]+)-([0-9\.]+\-?[a-z0-9]*)\s'
9
+ # Source: https://superuser.com/a/1472405
10
+ # Modified to return version and release inside a single group and removed extra capturing groups
11
+ APK_REGEX = r"(.+)-([^-]+-r[^-]+) \S+ \{\S+\} \(.+?\)"
6
12
 
7
13
 
8
14
  class ApkPackages(FactBase):
9
- '''
15
+ """
10
16
  Returns a dict of installed apk packages:
11
17
 
12
18
  .. code:: python
13
19
 
14
- 'package_name': ['version'],
15
- ...
16
- '''
20
+ {
21
+ "package_name": ["version"],
22
+ }
23
+ """
24
+
25
+ @override
26
+ def command(self) -> str:
27
+ return "apk list --installed"
28
+
29
+ @override
30
+ def requires_command(self) -> str:
31
+ return "apk"
17
32
 
18
- command = 'apk list --installed'
19
- deafult = dict
33
+ default = dict
20
34
 
35
+ @override
21
36
  def process(self, output):
22
37
  return parse_packages(APK_REGEX, output)
pyinfra/facts/apt.py CHANGED
@@ -1,12 +1,39 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
5
+ from typing_extensions import TypedDict, override
6
+
3
7
  from pyinfra.api import FactBase
4
8
 
5
- from .util.packaging import parse_packages
9
+ from .gpg import GpgFactBase
10
+ from .util import make_cat_files_command
11
+
12
+
13
+ def noninteractive_apt(command: str, force=False):
14
+ args = ["DEBIAN_FRONTEND=noninteractive apt-get -y"]
15
+
16
+ if force:
17
+ args.append("--force-yes")
18
+
19
+ args.extend(
20
+ (
21
+ '-o Dpkg::Options::="--force-confdef"',
22
+ '-o Dpkg::Options::="--force-confold"',
23
+ command,
24
+ ),
25
+ )
26
+
27
+ return " ".join(args)
28
+
29
+
30
+ APT_CHANGES_RE = re.compile(
31
+ r"^(\d+) upgraded, (\d+) newly installed, (\d+) to remove and (\d+) not upgraded.$"
32
+ )
6
33
 
7
34
 
8
35
  def parse_apt_repo(name):
9
- regex = r'^(deb(?:-src)?)(?:\s+\[([a-zA-Z0-9=,\s]+)\])?\s+([^\s]+)\s+([a-z-]+)\s+([a-z-\s]*)$'
36
+ regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([^\s]+)\s+([a-z-\s\d]*)$"
10
37
 
11
38
  matches = re.match(regex, name)
12
39
 
@@ -18,39 +45,51 @@ def parse_apt_repo(name):
18
45
  options_string = matches.group(2)
19
46
  if options_string:
20
47
  for option in options_string.split():
21
- key, value = option.split('=', 1)
22
- if ',' in value:
23
- value = value.split(',')
48
+ key, value = option.split("=", 1)
49
+ if "," in value:
50
+ value = value.split(",")
24
51
 
25
52
  options[key] = value
26
53
 
27
54
  return {
28
- 'options': options,
29
- 'type': matches.group(1),
30
- 'url': matches.group(3),
31
- 'distribution': matches.group(4),
32
- 'components': set(matches.group(5).split()),
55
+ "options": options,
56
+ "type": matches.group(1),
57
+ "url": matches.group(3),
58
+ "distribution": matches.group(4),
59
+ "components": list(matches.group(5).split()),
33
60
  }
34
61
 
35
62
 
36
63
  class AptSources(FactBase):
37
- '''
64
+ """
38
65
  Returns a list of installed apt sources:
39
66
 
40
67
  .. code:: python
41
68
 
42
- {
43
- 'type': 'deb',
44
- 'url': 'http://archive.ubuntu.org',
45
- 'distribution': 'trusty',
46
- 'components', ['main', 'multiverse']
47
- },
48
- ...
49
- '''
50
-
51
- command = 'cat /etc/apt/sources.list /etc/apt/sources.list.d/*.list || true'
69
+ [
70
+ {
71
+ "type": "deb",
72
+ "url": "http://archive.ubuntu.org",
73
+ "distribution": "trusty",
74
+ "components", ["main", "multiverse"],
75
+ },
76
+ ]
77
+ """
78
+
79
+ @override
80
+ def command(self) -> str:
81
+ return make_cat_files_command(
82
+ "/etc/apt/sources.list",
83
+ "/etc/apt/sources.list.d/*.list",
84
+ )
85
+
86
+ @override
87
+ def requires_command(self) -> str:
88
+ return "apt" # if apt installed, above should exist
89
+
52
90
  default = list
53
91
 
92
+ @override
54
93
  def process(self, output):
55
94
  repos = []
56
95
 
@@ -62,46 +101,64 @@ class AptSources(FactBase):
62
101
  return repos
63
102
 
64
103
 
65
- class DebPackages(FactBase):
66
- '''
67
- Returns a dict of installed dpkg packages:
104
+ class AptKeys(GpgFactBase):
105
+ """
106
+ Returns information on GPG keys apt has in its keychain:
68
107
 
69
108
  .. code:: python
70
109
 
71
- 'package_name': ['version'],
72
- ...
73
- '''
74
-
75
- command = 'dpkg -l'
76
- regex = r'^ii\s+([a-zA-Z0-9\+\-\.]+):?[a-zA-Z0-9]*\s+([a-zA-Z0-9:~\.\-\+]+).+$'
77
- default = dict
78
-
79
- def process(self, output):
80
- return parse_packages(self.regex, output)
81
-
82
-
83
- class DebPackage(FactBase):
84
- '''
85
- Returns information on a .deb file.
86
- '''
87
-
88
- _regexes = {
89
- 'name': r'^Package: ([a-zA-Z0-9\-]+)$',
90
- 'version': r'^Version: ([0-9\:\.\-]+)$',
91
- }
92
-
93
- def command(self, name):
94
- return 'dpkg -I {0}'.format(name)
95
-
96
- def process(self, output):
97
- data = {}
98
-
110
+ {
111
+ "KEY-ID": {
112
+ "length": 4096,
113
+ "uid": "Oxygem <hello@oxygem.com>"
114
+ },
115
+ }
116
+ """
117
+
118
+ # This requires both apt-key *and* apt-key itself requires gpg
119
+ @override
120
+ def command(self) -> str:
121
+ return "! command -v gpg || apt-key list --with-colons"
122
+
123
+ @override
124
+ def requires_command(self) -> str:
125
+ return "apt-key"
126
+
127
+
128
+ class AptSimulationDict(TypedDict):
129
+ upgraded: int
130
+ newly_installed: int
131
+ removed: int
132
+ not_upgraded: int
133
+
134
+
135
+ class SimulateOperationWillChange(FactBase[AptSimulationDict]):
136
+ """
137
+ Simulate an 'apt-get' operation and try to detect if any changes would be performed.
138
+ """
139
+
140
+ @override
141
+ def command(self, command: str) -> str:
142
+ # LC_ALL=C: Ensure the output is in english, as we want to parse it
143
+ return "LC_ALL=C " + noninteractive_apt(f"{command} --dry-run")
144
+
145
+ @override
146
+ def requires_command(self, command: str) -> str:
147
+ return "apt-get"
148
+
149
+ @override
150
+ def process(self, output) -> AptSimulationDict:
151
+ # We are looking for a line similar to
152
+ # "3 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
99
153
  for line in output:
100
- for key, regex in self._regexes.items():
101
- matches = re.match(regex, line)
102
- if matches:
103
- value = matches.group(1)
104
- data[key] = value
105
- break
106
-
107
- return data
154
+ result = APT_CHANGES_RE.match(line)
155
+ if result is not None:
156
+ return {
157
+ "upgraded": int(result[1]),
158
+ "newly_installed": int(result[2]),
159
+ "removed": int(result[3]),
160
+ "not_upgraded": int(result[4]),
161
+ }
162
+
163
+ # We did not find the line we expected:
164
+ raise Exception("Did not find proposed changes in output")
pyinfra/facts/brew.py CHANGED
@@ -1,46 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from typing_extensions import override
6
+
7
+ from pyinfra import logger
1
8
  from pyinfra.api import FactBase
2
9
 
3
10
  from .util.packaging import parse_packages
4
11
 
5
- BREW_REGEX = r'^([a-zA-Z\-]+)\s([0-9\._+a-z\-]+)'
12
+ BREW_REGEX = r"^([^\s]+)\s([0-9\._+a-z\-]+)"
13
+
14
+
15
+ def new_cask_cli(version):
16
+ """
17
+ Returns true if brew is version 2.6.0 or later and thus has the new CLI for casks.
18
+ i.e. we need to use brew list --cask instead of brew cask list
19
+ See https://brew.sh/2020/12/01/homebrew-2.6.0/
20
+ The version string returned by BrewVersion is a list of major, minor, patch version numbers
21
+ """
22
+ return (version[0] >= 3) or ((version[0] >= 2) and version[1] >= 6)
23
+
24
+
25
+ VERSION_MATCHER = re.compile(r"^Homebrew\s+(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+).*$")
26
+
27
+
28
+ def unknown_version():
29
+ return [0, 0, 0]
30
+
31
+
32
+ class BrewVersion(FactBase):
33
+ """
34
+ Returns the version of brew installed as a semantic versioning tuple:
35
+
36
+ .. code:: python
37
+
38
+ [major, minor, patch]
39
+
40
+ """
41
+
42
+ @override
43
+ def command(self) -> str:
44
+ return "brew --version"
45
+
46
+ @override
47
+ def requires_command(self) -> str:
48
+ return "brew"
49
+
50
+ @override
51
+ @staticmethod
52
+ def default():
53
+ return [0, 0, 0]
54
+
55
+ @override
56
+ def process(self, output):
57
+ out = list(output)[0]
58
+ m = VERSION_MATCHER.match(out)
59
+ if m is not None:
60
+ return [int(m.group(key)) for key in ["major", "minor", "patch"]]
61
+ logger.warning("could not parse version string from brew: %s", out)
62
+ return self.default()
6
63
 
7
64
 
8
65
  class BrewPackages(FactBase):
9
- '''
66
+ """
10
67
  Returns a dict of installed brew packages:
11
68
 
12
69
  .. code:: python
13
70
 
14
- 'package_name': ['version'],
15
- ...
16
- '''
71
+ {
72
+ "package_name": ["version"],
73
+ }
74
+ """
75
+
76
+ @override
77
+ def command(self) -> str:
78
+ return "brew list --versions"
79
+
80
+ @override
81
+ def requires_command(self) -> str:
82
+ return "brew"
17
83
 
18
- command = 'brew list --versions'
19
- deafult = dict
84
+ default = dict
20
85
 
86
+ @override
21
87
  def process(self, output):
22
88
  return parse_packages(BREW_REGEX, output)
23
89
 
24
90
 
25
91
  class BrewCasks(BrewPackages):
26
- '''
92
+ """
27
93
  Returns a dict of installed brew casks:
28
94
 
29
95
  .. code:: python
30
96
 
31
- 'package_name': ['version'],
32
- ...
33
- '''
97
+ {
98
+ "package_name": ["version"],
99
+ }
100
+ """
34
101
 
35
- command = 'brew cask list --versions'
102
+ @override
103
+ def command(self) -> str:
104
+ return (
105
+ r'if brew --version | grep -q -e "Homebrew\ +(1\.|2\.[0-5]).*" 1>/dev/null;'
106
+ r"then brew cask list --versions; else brew list --cask --versions; fi"
107
+ )
108
+
109
+ @override
110
+ def requires_command(self) -> str:
111
+ return "brew"
36
112
 
37
113
 
38
114
  class BrewTaps(FactBase):
39
- '''
115
+ """
40
116
  Returns a list of brew taps.
41
- '''
117
+ """
118
+
119
+ @override
120
+ def command(self) -> str:
121
+ return "brew tap"
122
+
123
+ @override
124
+ def requires_command(self) -> str:
125
+ return "brew"
42
126
 
43
- command = 'brew tap'
127
+ default = list
44
128
 
129
+ @override
45
130
  def process(self, output):
46
131
  return output
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
5
+ from .sysvinit import InitdStatus
6
+
7
+
8
+ class RcdStatus(InitdStatus):
9
+ """
10
+ Same as ``initd_status`` but for BSD (/etc/rc.d) systems. Unlike Linux/init.d,
11
+ BSD init scripts are well behaved and as such their output can be trusted.
12
+ """
13
+
14
+ @override
15
+ def command(self) -> str:
16
+ return """
17
+ for SERVICE in `find /etc/rc.d /usr/local/etc/rc.d -type f`; do
18
+ $SERVICE status 2> /dev/null || $SERVICE check 2> /dev/null
19
+ echo "`basename $SERVICE`=$?"
20
+ done
21
+ """
22
+
23
+ default = dict
pyinfra/facts/cargo.py ADDED
@@ -0,0 +1,37 @@
1
+ # encoding: utf8
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing_extensions import override
6
+
7
+ from pyinfra.api import FactBase
8
+
9
+ from .util.packaging import parse_packages
10
+
11
+ CARGO_REGEX = r"^([a-zA-Z0-9\-]+)\sv([0-9\.]+)"
12
+
13
+
14
+ class CargoPackages(FactBase):
15
+ """
16
+ Returns a dict of installed cargo packages globally:
17
+
18
+ .. code:: python
19
+
20
+ {
21
+ "package_name": ["version"],
22
+ }
23
+ """
24
+
25
+ default = dict
26
+
27
+ @override
28
+ def command(self) -> str:
29
+ return "cargo install --list"
30
+
31
+ @override
32
+ def requires_command(self) -> str:
33
+ return "cargo"
34
+
35
+ @override
36
+ def process(self, output):
37
+ return parse_packages(CARGO_REGEX, output)
pyinfra/facts/choco.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
5
+ from pyinfra.api import FactBase
6
+
7
+ from .util.packaging import parse_packages
8
+
9
+ CHOCO_REGEX = r"^([a-zA-Z0-9\.\-\+\_]+)\s([0-9\.]+)$"
10
+
11
+
12
+ class ChocoPackages(FactBase):
13
+ """
14
+ Returns a dict of installed choco (Chocolatey) packages:
15
+
16
+ .. code:: python
17
+
18
+ {
19
+ "package_name": ["version"],
20
+ }
21
+ """
22
+
23
+ @override
24
+ def command(self) -> str:
25
+ return "choco list"
26
+
27
+ shell_executable = "ps"
28
+
29
+ default = dict
30
+
31
+ @override
32
+ def process(self, output):
33
+ return parse_packages(CHOCO_REGEX, output)
34
+
35
+
36
+ class ChocoVersion(FactBase):
37
+ """
38
+ Returns the choco (Chocolatey) version.
39
+ """
40
+
41
+ @override
42
+ def command(self) -> str:
43
+ return "choco --version"
44
+
45
+ @override
46
+ def process(self, output):
47
+ return "".join(output).replace("\n", "")