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_cli/inventory.py CHANGED
@@ -1,129 +1,271 @@
1
+ import socket
2
+ from collections import defaultdict
1
3
  from os import listdir, path
2
- from types import GeneratorType
4
+ from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union
3
5
 
4
- from pyinfra import logger, pseudo_inventory
6
+ from pyinfra import logger
5
7
  from pyinfra.api.inventory import Inventory
6
- from pyinfra_cli.util import exec_file
8
+ from pyinfra.connectors.sshuserclient.client import get_ssh_config
9
+ from pyinfra.context import ctx_inventory
10
+
11
+ from .exceptions import CliError
12
+ from .util import exec_file, try_import_module_attribute
13
+
14
+ HostType = Union[str, Tuple[str, Dict]]
7
15
 
8
16
  # Hosts in an inventory can be just the hostname or a tuple (hostname, data)
9
17
  ALLOWED_HOST_TYPES = (str, tuple)
10
18
 
11
- # Group data can be any "core" Python type
12
- ALLOWED_DATA_TYPES = tuple(
13
- (int,)
14
- + (str, bytes)
15
- + (bool, dict, list, set, tuple, float, complex),
16
- )
17
19
 
18
-
19
- def _is_inventory_group(key, value):
20
- '''
20
+ def _is_inventory_group(key: str, value: Any):
21
+ """
21
22
  Verify that a module-level variable (key = value) is a valid inventory group.
22
- '''
23
+ """
23
24
 
24
- if (
25
- key.startswith('_')
26
- or not isinstance(value, (list, tuple, GeneratorType))
27
- ):
25
+ if key.startswith("__"):
26
+ # Ignore __builtins__/__file__
27
+ return False
28
+ elif key.startswith("_"):
29
+ logger.debug(
30
+ 'Ignoring variable "%s" in inventory file since it starts with a leading underscore',
31
+ key,
32
+ )
28
33
  return False
29
34
 
30
- # If the group is a tuple of (hosts, data), check the hosts
31
- if isinstance(value, tuple):
35
+ if isinstance(value, list):
36
+ pass
37
+ elif isinstance(value, tuple):
38
+ # If the group is a tuple of (hosts, data), check the hosts
32
39
  value = value[0]
40
+ else:
41
+ logger.debug(
42
+ 'Ignoring variable "%s" in inventory file since it is not a list or tuple',
43
+ key,
44
+ )
45
+ return False
33
46
 
34
- # Expand any generators of hosts
35
- if isinstance(value, GeneratorType):
36
- value = list(value)
37
-
38
- return all(
39
- isinstance(item, ALLOWED_HOST_TYPES)
40
- for item in value
41
- )
42
-
43
-
44
- def _is_group_data(key, value):
45
- '''
46
- Verify that a module-level variable (key = value) is a valid bit of group data.
47
- '''
47
+ if not all(isinstance(item, ALLOWED_HOST_TYPES) for item in value):
48
+ logger.warning(
49
+ 'Ignoring host group "%s". '
50
+ "Host groups may only contain strings (host) or tuples (host, data).",
51
+ key,
52
+ )
53
+ return False
48
54
 
49
- return (
50
- isinstance(value, ALLOWED_DATA_TYPES)
51
- and not key.startswith('_')
52
- )
55
+ return True
53
56
 
54
57
 
55
- def _get_group_data(deploy_dir):
58
+ def _get_group_data(dirname_or_filename: str):
56
59
  group_data = {}
57
- group_data_directory = path.join(deploy_dir, 'group_data')
58
60
 
59
- if path.exists(group_data_directory):
60
- files = listdir(group_data_directory)
61
+ logger.debug("Checking possible group_data at: %s", dirname_or_filename)
62
+
63
+ if path.exists(dirname_or_filename):
64
+ if path.isfile(dirname_or_filename):
65
+ files = [dirname_or_filename]
66
+ else:
67
+ files = [path.join(dirname_or_filename, file) for file in listdir(dirname_or_filename)]
61
68
 
62
69
  for file in files:
63
- if not file.endswith('.py'):
70
+ if not file.endswith(".py"):
64
71
  continue
65
72
 
66
- group_data_file = path.join(group_data_directory, file)
67
73
  group_name = path.basename(file)[:-3]
68
74
 
69
- logger.debug('Looking for group data in: {0}'.format(group_data_file))
75
+ logger.debug("Looking for group data in: %s", file)
70
76
 
71
77
  # Read the files locals into a dict
72
- attrs = exec_file(group_data_file, return_locals=True)
78
+ attrs = exec_file(file, return_locals=True)
79
+ keys = attrs.get("__all__", attrs.keys())
73
80
 
74
81
  group_data[group_name] = {
75
82
  key: value
76
83
  for key, value in attrs.items()
77
- if _is_group_data(key, value)
84
+ if key in keys and not key.startswith("__")
78
85
  }
79
86
 
80
87
  return group_data
81
88
 
82
89
 
83
- def _get_groups_from_filename(inventory_filename):
90
+ def _get_groups_from_filename(inventory_filename: str):
84
91
  attrs = exec_file(inventory_filename, return_locals=True)
85
92
 
86
- return {
87
- key: value
88
- for key, value in attrs.items()
89
- if _is_inventory_group(key, value)
90
- }
93
+ return {key: value for key, value in attrs.items() if _is_inventory_group(key, value)}
94
+
95
+
96
+ T = TypeVar("T")
97
+
98
+
99
+ def _get_any_tuple_first(item: Union[T, Tuple[T, Any]]) -> T:
100
+ return item[0] if isinstance(item, tuple) else item
101
+
102
+
103
+ def _resolves_to_host(maybe_host: str) -> bool:
104
+ """Check if a string resolves to a valid IP address."""
105
+ try:
106
+ # Use getaddrinfo to support IPv6 hosts
107
+ socket.getaddrinfo(maybe_host, port=None)
108
+ return True
109
+ except socket.gaierror:
110
+ alias = _get_ssh_alias(maybe_host)
111
+ if not alias:
112
+ return False
113
+
114
+ try:
115
+ socket.getaddrinfo(alias, port=None)
116
+ return True
117
+ except socket.gaierror:
118
+ return False
119
+
120
+
121
+ def _get_ssh_alias(maybe_host: str) -> Optional[str]:
122
+ logger.debug('Checking if "%s" is an SSH alias', maybe_host)
123
+
124
+ # Note this does not cover the case where `host.data.ssh_config_file` is used
125
+ ssh_config = get_ssh_config()
126
+
127
+ if ssh_config is None:
128
+ logger.debug("Could not load SSH config")
129
+ return None
130
+
131
+ options = ssh_config.lookup(maybe_host)
132
+ alias = options.get("hostname")
133
+
134
+ if alias is None or maybe_host == alias:
135
+ return None
136
+
137
+ return alias
91
138
 
92
139
 
93
140
  def make_inventory(
94
- inventory_filename,
95
- deploy_dir=None,
96
- ssh_port=None,
97
- ssh_user=None,
98
- ssh_key=None,
99
- ssh_key_password=None,
100
- ssh_password=None,
141
+ inventory: str,
142
+ override_data=None,
143
+ cwd: Optional[str] = None,
144
+ group_data_directories=None,
145
+ ):
146
+ # (Un)fortunately the CLI is pretty flexible for inventory inputs; we support inventory files, a
147
+ # single hostname, list of hosts, connectors, and python module.function or module:function
148
+ # imports.
149
+ #
150
+ # We check first for an inventory file, a list of hosts or anything with a connector, because
151
+ # (1) an inventory file is a common use case and (2) no other option can have a comma or an @
152
+ # symbol in them.
153
+ is_path_or_host_list_or_connector = (
154
+ path.exists(inventory)
155
+ or "," in inventory
156
+ or "@" in inventory
157
+ # Special case: passing an arbitrary name and specifying --data ssh_hostname=a.b.c
158
+ or (override_data is not None and "ssh_hostname" in override_data)
159
+ )
160
+ if not is_path_or_host_list_or_connector:
161
+ # Next, try loading the inventory from a python function. This happens before checking for a
162
+ # single-host inventory, so that your command does not stop working because somebody
163
+ # registered the domain `my.module.name`.
164
+ inventory_func = try_import_module_attribute(inventory, raise_for_none=False)
165
+
166
+ # If the inventory does not refer to a module, we finally check if it refers to a reachable
167
+ # host
168
+ if inventory_func is None and _resolves_to_host(inventory):
169
+ is_path_or_host_list_or_connector = True
170
+
171
+ if is_path_or_host_list_or_connector:
172
+ # The inventory is either an inventory file or a (list of) hosts
173
+ return make_inventory_from_files(inventory, override_data, cwd, group_data_directories)
174
+ elif inventory_func is None:
175
+ logger.warning(
176
+ f"{inventory} is neither an inventory file, a (list of) hosts or connectors "
177
+ "nor refers to a python module"
178
+ )
179
+ return Inventory.empty()
180
+ else:
181
+ return make_inventory_from_func(inventory_func, override_data)
182
+
183
+
184
+ def make_inventory_from_func(
185
+ inventory_func: Callable[[], Dict[str, List[HostType]]],
186
+ override_data: Optional[Dict[Any, Any]] = None,
101
187
  ):
102
- '''
188
+ logger.warning("Loading inventory via import function is in alpha!")
189
+
190
+ try:
191
+ groups = inventory_func()
192
+ except Exception as e:
193
+ raise CliError(f"Failed to load inventory function: {inventory_func.__name__}: {e}")
194
+
195
+ if not isinstance(groups, dict):
196
+ raise TypeError(f"Inventory function {inventory_func.__name__} did not return a dictionary")
197
+
198
+ # TODO: this shouldn't be required to make an inventory, groups should suffice
199
+ combined_host_list = set()
200
+ groups_with_data: Dict[str, Tuple[List[HostType], Dict]] = {}
201
+
202
+ for key, hosts in groups.items():
203
+ data: Dict = {}
204
+
205
+ if isinstance(hosts, tuple):
206
+ hosts, data = hosts
207
+
208
+ if not isinstance(data, dict):
209
+ raise TypeError(
210
+ f"Inventory function {inventory_func.__name__} "
211
+ f"group contains non-dictionary data: {key}"
212
+ )
213
+
214
+ for host in hosts:
215
+ if not isinstance(host, ALLOWED_HOST_TYPES):
216
+ raise TypeError(
217
+ f"Inventory function {inventory_func.__name__} invalid host: {host}"
218
+ )
219
+
220
+ host = _get_any_tuple_first(host)
221
+
222
+ if not isinstance(host, str):
223
+ raise TypeError(
224
+ f"Inventory function {inventory_func.__name__} invalid host name: {host}"
225
+ )
226
+
227
+ combined_host_list.add(host)
228
+
229
+ groups_with_data[key] = (hosts, data)
230
+
231
+ return Inventory(
232
+ (list(combined_host_list), {}),
233
+ override_data=override_data,
234
+ **groups_with_data,
235
+ )
236
+
237
+
238
+ def make_inventory_from_files(
239
+ inventory_filename: str,
240
+ override_data=None,
241
+ cwd: Optional[str] = None,
242
+ group_data_directories=None,
243
+ ):
244
+ """
103
245
  Builds a ``pyinfra.api.Inventory`` from the filesystem. If the file does not exist
104
246
  and doesn't contain a / attempts to use that as the only hostname.
105
- '''
106
-
107
- if ssh_port is not None:
108
- ssh_port = int(ssh_port)
247
+ """
109
248
 
110
249
  file_groupname = None
111
250
 
251
+ # TODO: this type is complex & convoluted, fix this
252
+ groups: Dict[str, Union[List[str], Tuple[List[str], Dict[str, Any]]]]
253
+
112
254
  # If we're not a valid file we assume a list of comma separated hostnames
113
255
  if not path.exists(inventory_filename):
114
256
  groups = {
115
- 'all': inventory_filename.split(','),
257
+ "all": inventory_filename.split(","),
116
258
  }
117
259
  else:
118
260
  groups = _get_groups_from_filename(inventory_filename)
119
261
  # Used to set all the hosts to an additional group - that of the filename
120
262
  # ie inventories/dev.py means all the hosts are in the dev group, if not present
121
- file_groupname = path.basename(inventory_filename).rsplit('.')[0]
263
+ file_groupname = path.basename(inventory_filename).rsplit(".", 1)[0]
122
264
 
123
- all_data = {}
265
+ all_data: Dict[str, Any] = {}
124
266
 
125
- if 'all' in groups:
126
- all_hosts = groups.pop('all')
267
+ if "all" in groups:
268
+ all_hosts = groups.pop("all")
127
269
 
128
270
  if isinstance(all_hosts, tuple):
129
271
  all_hosts, all_data = all_hosts
@@ -133,16 +275,15 @@ def make_inventory(
133
275
  all_hosts = []
134
276
  for hosts in groups.values():
135
277
  # Groups can be a list of hosts or tuple of (hosts, data)
136
- hosts = hosts[0] if isinstance(hosts, tuple) else hosts
137
-
278
+ hosts = _get_any_tuple_first(hosts)
138
279
  for host in hosts:
139
280
  # Hosts can be a hostname or tuple of (hostname, data)
140
- hostname = host[0] if isinstance(host, tuple) else host
281
+ hostname = _get_any_tuple_first(host)
141
282
 
142
283
  if hostname not in all_hosts:
143
284
  all_hosts.append(hostname)
144
285
 
145
- groups['all'] = (all_hosts, all_data)
286
+ groups["all"] = (all_hosts, all_data)
146
287
 
147
288
  # Apply the filename group if not already defined
148
289
  if file_groupname and file_groupname not in groups:
@@ -152,8 +293,8 @@ def make_inventory(
152
293
  # mode we want to be define this in separate files (inventory / group data). The
153
294
  # issue is we want inventory access within the group data files - but at this point
154
295
  # we're not ready to make an Inventory. So here we just create a fake one, and
155
- # attach it to pseudo_inventory while we import the data files.
156
- logger.debug('Creating fake inventory...')
296
+ # attach it to the inventory context while we import the data files.
297
+ logger.debug("Creating fake inventory...")
157
298
 
158
299
  fake_groups = {
159
300
  # In API mode groups *must* be tuples of (hostnames, data)
@@ -161,13 +302,24 @@ def make_inventory(
161
302
  for name, group in groups.items()
162
303
  }
163
304
  fake_inventory = Inventory((all_hosts, all_data), **fake_groups)
164
- pseudo_inventory.set(fake_inventory)
165
305
 
166
- # Get all group data (group_data/*.py)
167
- group_data = _get_group_data(deploy_dir)
306
+ possible_group_data_folders = []
307
+ if cwd:
308
+ possible_group_data_folders.append(path.join(cwd, "group_data"))
309
+ inventory_dirname = path.abspath(path.dirname(inventory_filename))
310
+ if inventory_dirname != cwd:
311
+ possible_group_data_folders.append(path.join(inventory_dirname, "group_data"))
168
312
 
169
- # Reset the pseudo inventory
170
- pseudo_inventory.reset()
313
+ if group_data_directories:
314
+ possible_group_data_folders.extend(group_data_directories)
315
+
316
+ group_data: Dict[str, Dict[str, Any]] = defaultdict(dict)
317
+
318
+ with ctx_inventory.use(fake_inventory):
319
+ for folder in possible_group_data_folders:
320
+ for group_name, data in _get_group_data(folder).items():
321
+ group_data[group_name].update(data)
322
+ logger.debug("Adding data to group %s: %r", group_name, data)
171
323
 
172
324
  # For each group load up any data
173
325
  for name, hosts in groups.items():
@@ -188,12 +340,4 @@ def make_inventory(
188
340
  for name, data in group_data.items():
189
341
  groups[name] = ([], data)
190
342
 
191
- return Inventory(
192
- groups.pop('all'),
193
- ssh_user=ssh_user,
194
- ssh_key=ssh_key,
195
- ssh_key_password=ssh_key_password,
196
- ssh_port=ssh_port,
197
- ssh_password=ssh_password,
198
- **groups
199
- ), file_groupname and file_groupname.lower()
343
+ return Inventory(groups.pop("all"), override_data=override_data, **groups)
pyinfra_cli/log.py CHANGED
@@ -1,30 +1,33 @@
1
1
  import logging
2
- import sys
3
2
 
4
3
  import click
4
+ from typing_extensions import override
5
5
 
6
- from pyinfra import logger
6
+ from pyinfra import logger, state
7
+ from pyinfra.context import ctx_state
7
8
 
8
- STDOUT_LOG_LEVELS = (logging.DEBUG, logging.INFO)
9
- STDERR_LOG_LEVELS = (logging.WARNING, logging.ERROR, logging.CRITICAL)
10
9
 
11
-
12
- class LogFilter(logging.Filter):
13
- def __init__(self, *levels):
14
- self.levels = levels
15
-
16
- def filter(self, record):
17
- return record.levelno in self.levels
10
+ class LogHandler(logging.Handler):
11
+ @override
12
+ def emit(self, record):
13
+ try:
14
+ message = self.format(record)
15
+ click.echo(message, err=True)
16
+ except Exception:
17
+ self.handleError(record)
18
18
 
19
19
 
20
20
  class LogFormatter(logging.Formatter):
21
+ previous_was_header = True
22
+
21
23
  level_to_format = {
22
- logging.DEBUG: lambda s: click.style(s, 'green'),
23
- logging.WARNING: lambda s: click.style(s, 'yellow'),
24
- logging.ERROR: lambda s: click.style(s, 'red'),
25
- logging.CRITICAL: lambda s: click.style(s, 'red', bold=True),
24
+ logging.DEBUG: lambda s: click.style(s, "green"),
25
+ logging.WARNING: lambda s: click.style(s, "yellow"),
26
+ logging.ERROR: lambda s: click.style(s, "red"),
27
+ logging.CRITICAL: lambda s: click.style(s, "red", bold=True),
26
28
  }
27
29
 
30
+ @override
28
31
  def format(self, record):
29
32
  message = record.msg
30
33
 
@@ -33,48 +36,41 @@ class LogFormatter(logging.Formatter):
33
36
 
34
37
  # Add path/module info for debug
35
38
  if record.levelno is logging.DEBUG:
36
- path_start = record.pathname.rfind('pyinfra')
39
+ path_start = record.pathname.rfind("pyinfra")
37
40
 
38
41
  if path_start:
39
42
  pyinfra_path = record.pathname[path_start:-3] # -3 removes `.py`
40
- module_name = pyinfra_path.replace('/', '.')
41
- message = '[{0}] {1}'.format(module_name, message)
43
+ module_name = pyinfra_path.replace("/", ".")
44
+ message = "[{0}] {1}".format(module_name, message)
42
45
 
43
46
  # We only handle strings here
44
47
  if isinstance(message, str):
45
- if '-->' not in message:
46
- message = ' {0}'.format(message)
48
+ if ctx_state.isset() and record.levelno is logging.WARNING:
49
+ state.increment_warning_counter()
50
+
51
+ if "-->" in message:
52
+ if not self.previous_was_header:
53
+ click.echo(err=True)
54
+ else:
55
+ message = " {0}".format(message)
47
56
 
48
57
  if record.levelno in self.level_to_format:
49
58
  message = self.level_to_format[record.levelno](message)
50
59
 
60
+ self.previous_was_header = "-->" in message
51
61
  return message
52
62
 
53
63
  # If not a string, pass to standard Formatter
54
- else:
55
- return super(LogFormatter, self).format(record)
56
-
64
+ return super().format(record)
57
65
 
58
- def setup_logging(log_level):
59
- # Set the log level
60
- logger.setLevel(log_level)
61
-
62
- # Setup a new handler for stdout & stderr
63
- stdout_handler = logging.StreamHandler(sys.stdout)
64
- stderr_handler = logging.StreamHandler(sys.stderr)
65
-
66
- # Setup filters to push different levels to different streams
67
- stdout_filter = LogFilter(*STDOUT_LOG_LEVELS)
68
- stdout_handler.addFilter(stdout_filter)
69
66
 
70
- stderr_filter = LogFilter(*STDERR_LOG_LEVELS)
71
- stderr_handler.addFilter(stderr_filter)
67
+ def setup_logging(log_level, other_log_level=None):
68
+ if other_log_level:
69
+ logging.basicConfig(level=other_log_level)
72
70
 
73
- # Setup a formatter
71
+ logger.setLevel(log_level)
72
+ handler = LogHandler()
74
73
  formatter = LogFormatter()
75
- stdout_handler.setFormatter(formatter)
76
- stderr_handler.setFormatter(formatter)
77
-
78
- # Add the handlers
79
- logger.addHandler(stdout_handler)
80
- logger.addHandler(stderr_handler)
74
+ handler.setFormatter(formatter)
75
+ logger.addHandler(handler)
76
+ logger.propagate = False