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.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +19 -3
- pyinfra/api/arguments.py +413 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +73 -18
- pyinfra/api/facts.py +267 -200
- pyinfra/api/host.py +416 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +309 -369
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +405 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +727 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +417 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +629 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +762 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +99 -0
- pyinfra/operations/apt.py +496 -0
- pyinfra/operations/brew.py +232 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +194 -0
- pyinfra/operations/dnf.py +213 -0
- pyinfra/operations/docker.py +492 -0
- pyinfra/operations/files.py +2014 -0
- pyinfra/operations/flatpak.py +95 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +48 -0
- pyinfra/operations/git.py +420 -0
- pyinfra/operations/iptables.py +312 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +69 -0
- pyinfra/operations/mysql.py +610 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +89 -0
- pyinfra/operations/pacman.py +82 -0
- pyinfra/operations/pip.py +206 -0
- pyinfra/operations/pipx.py +103 -0
- pyinfra/operations/pkg.py +71 -0
- pyinfra/operations/pkgin.py +92 -0
- pyinfra/operations/postgres.py +437 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +41 -0
- pyinfra/operations/python.py +73 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +190 -0
- pyinfra/operations/server.py +1100 -0
- pyinfra/operations/snap.py +118 -0
- pyinfra/operations/ssh.py +217 -0
- pyinfra/operations/systemd.py +150 -0
- pyinfra/operations/sysvinit.py +142 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +407 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +338 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +78 -0
- pyinfra/operations/yum.py +213 -0
- pyinfra/operations/zfs.py +176 -0
- pyinfra/operations/zypper.py +193 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.6.dist-info/METADATA +142 -0
- pyinfra-3.6.dist-info/RECORD +160 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
- pyinfra-3.6.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +793 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /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
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union
|
|
3
5
|
|
|
4
|
-
from pyinfra import logger
|
|
6
|
+
from pyinfra import logger
|
|
5
7
|
from pyinfra.api.inventory import Inventory
|
|
6
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
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
|
-
|
|
60
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
126
|
-
all_hosts = groups.pop(
|
|
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 =
|
|
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 =
|
|
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[
|
|
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
|
|
156
|
-
logger.debug(
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
23
|
-
logging.WARNING: lambda s: click.style(s,
|
|
24
|
-
logging.ERROR: lambda s: click.style(s,
|
|
25
|
-
logging.CRITICAL: lambda s: click.style(s,
|
|
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(
|
|
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 =
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
71
|
+
logger.setLevel(log_level)
|
|
72
|
+
handler = LogHandler()
|
|
74
73
|
formatter = LogFormatter()
|
|
75
|
-
|
|
76
|
-
|
|
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
|