pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__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.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +115 -97
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +5 -3
- pyinfra/api/config.py +139 -39
- pyinfra/api/connectors.py +5 -2
- pyinfra/api/deploy.py +19 -19
- pyinfra/api/exceptions.py +35 -4
- pyinfra/api/facts.py +62 -86
- pyinfra/api/host.py +102 -15
- pyinfra/api/inventory.py +4 -0
- pyinfra/api/operation.py +184 -118
- pyinfra/api/operations.py +66 -113
- pyinfra/api/state.py +53 -34
- pyinfra/api/util.py +64 -33
- pyinfra/connectors/base.py +65 -20
- pyinfra/connectors/chroot.py +15 -13
- pyinfra/connectors/docker.py +62 -72
- pyinfra/connectors/dockerssh.py +20 -19
- pyinfra/connectors/local.py +32 -22
- pyinfra/connectors/ssh.py +162 -86
- pyinfra/connectors/sshuserclient/client.py +1 -1
- pyinfra/connectors/terraform.py +57 -39
- pyinfra/connectors/util.py +26 -27
- pyinfra/connectors/vagrant.py +27 -26
- pyinfra/context.py +1 -0
- pyinfra/facts/apk.py +7 -2
- pyinfra/facts/apt.py +15 -7
- pyinfra/facts/brew.py +28 -13
- pyinfra/facts/bsdinit.py +9 -6
- pyinfra/facts/cargo.py +6 -3
- pyinfra/facts/choco.py +8 -4
- pyinfra/facts/deb.py +21 -9
- pyinfra/facts/dnf.py +11 -6
- pyinfra/facts/docker.py +30 -5
- pyinfra/facts/files.py +49 -33
- pyinfra/facts/gem.py +7 -2
- pyinfra/facts/git.py +14 -21
- pyinfra/facts/gpg.py +4 -1
- pyinfra/facts/hardware.py +186 -138
- pyinfra/facts/launchd.py +7 -2
- pyinfra/facts/lxd.py +8 -2
- pyinfra/facts/mysql.py +19 -12
- pyinfra/facts/npm.py +3 -1
- pyinfra/facts/openrc.py +8 -2
- pyinfra/facts/pacman.py +13 -5
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +5 -1
- pyinfra/facts/pkgin.py +7 -2
- pyinfra/facts/postgres.py +170 -0
- pyinfra/facts/postgresql.py +5 -162
- pyinfra/facts/rpm.py +21 -15
- pyinfra/facts/runit.py +70 -0
- pyinfra/facts/selinux.py +12 -4
- pyinfra/facts/server.py +240 -82
- pyinfra/facts/snap.py +8 -2
- pyinfra/facts/systemd.py +37 -13
- pyinfra/facts/sysvinit.py +7 -4
- pyinfra/facts/upstart.py +7 -2
- pyinfra/facts/util/packaging.py +3 -2
- pyinfra/facts/vzctl.py +8 -4
- pyinfra/facts/xbps.py +7 -2
- pyinfra/facts/yum.py +10 -5
- pyinfra/facts/zypper.py +9 -4
- pyinfra/operations/apk.py +5 -3
- pyinfra/operations/apt.py +28 -25
- pyinfra/operations/brew.py +60 -29
- pyinfra/operations/bsdinit.py +6 -4
- pyinfra/operations/cargo.py +3 -1
- pyinfra/operations/choco.py +3 -1
- pyinfra/operations/dnf.py +16 -20
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +187 -168
- pyinfra/operations/gem.py +3 -1
- pyinfra/operations/git.py +23 -25
- pyinfra/operations/iptables.py +33 -25
- pyinfra/operations/launchd.py +5 -6
- pyinfra/operations/lxd.py +7 -4
- pyinfra/operations/mysql.py +59 -55
- pyinfra/operations/npm.py +8 -1
- pyinfra/operations/openrc.py +5 -3
- pyinfra/operations/pacman.py +6 -7
- pyinfra/operations/pip.py +19 -12
- pyinfra/operations/pkg.py +3 -1
- pyinfra/operations/pkgin.py +5 -3
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -335
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -29
- pyinfra/operations/server.py +138 -67
- pyinfra/operations/snap.py +3 -1
- pyinfra/operations/ssh.py +18 -16
- pyinfra/operations/systemd.py +18 -12
- pyinfra/operations/sysvinit.py +7 -5
- pyinfra/operations/upstart.py +7 -5
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +177 -0
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +54 -38
- pyinfra/operations/util/service.py +39 -47
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +5 -3
- pyinfra/operations/yum.py +15 -19
- pyinfra/operations/zypper.py +9 -10
- pyinfra/version.py +5 -2
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/METADATA +51 -58
- pyinfra-3.0.1.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/entry_points.txt +0 -3
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +3 -2
- pyinfra_cli/exceptions.py +75 -43
- pyinfra_cli/inventory.py +52 -31
- pyinfra_cli/log.py +10 -2
- pyinfra_cli/main.py +88 -65
- pyinfra_cli/prints.py +37 -109
- pyinfra_cli/util.py +15 -10
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +9 -9
- tests/test_api/test_api_deploys.py +15 -19
- tests/test_api/test_api_facts.py +4 -5
- tests/test_api/test_api_operations.py +18 -20
- tests/test_api/test_api_util.py +41 -2
- tests/test_cli/test_cli.py +14 -50
- tests/test_cli/test_cli_deploy.py +10 -12
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/test_cli_inventory.py +66 -0
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_dockerssh.py +11 -8
- tests/test_connectors/test_ssh.py +88 -23
- tests/test_connectors/test_sshuserclient.py +1 -1
- tests/test_connectors/test_terraform.py +11 -8
- tests/test_connectors/test_vagrant.py +6 -6
- pyinfra/connectors/ansible.py +0 -175
- pyinfra/connectors/mech.py +0 -189
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -312
- pyinfra/facts/windows.py +0 -366
- pyinfra/facts/windows_files.py +0 -90
- pyinfra/operations/windows.py +0 -59
- pyinfra/operations/windows_files.py +0 -538
- pyinfra-3.0.dev0.dist-info/RECORD +0 -170
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
pyinfra/api/config.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import importlib_metadata
|
|
3
|
+
except ImportError:
|
|
4
|
+
import importlib.metadata as importlib_metadata # type: ignore[no-redef]
|
|
1
5
|
from os import path
|
|
6
|
+
from typing import Iterable, Optional, Set
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
from
|
|
8
|
+
from packaging.markers import Marker
|
|
9
|
+
from packaging.requirements import Requirement
|
|
10
|
+
from packaging.specifiers import SpecifierSet
|
|
11
|
+
from packaging.version import Version
|
|
5
12
|
|
|
6
13
|
from pyinfra import __version__, state
|
|
7
14
|
|
|
@@ -10,37 +17,40 @@ from .exceptions import PyinfraError
|
|
|
10
17
|
|
|
11
18
|
class ConfigDefaults:
|
|
12
19
|
# % of hosts which have to fail for all operations to stop
|
|
13
|
-
FAIL_PERCENT = None
|
|
20
|
+
FAIL_PERCENT: Optional[int] = None
|
|
14
21
|
# Seconds to timeout SSH connections
|
|
15
|
-
CONNECT_TIMEOUT = 10
|
|
16
|
-
# Temporary directory (on the remote side) to use for caching any files/downloads
|
|
17
|
-
|
|
22
|
+
CONNECT_TIMEOUT: int = 10
|
|
23
|
+
# Temporary directory (on the remote side) to use for caching any files/downloads, the default
|
|
24
|
+
# None value first tries to load the hosts' temporary directory configured via "TMPDIR" env
|
|
25
|
+
# variable, falling back to DEFAULT_TEMP_DIR if not set.
|
|
26
|
+
TEMP_DIR: Optional[str] = None
|
|
27
|
+
DEFAULT_TEMP_DIR: str = "/tmp"
|
|
18
28
|
# Gevent pool size (defaults to #of target hosts)
|
|
19
|
-
PARALLEL = 0
|
|
29
|
+
PARALLEL: int = 0
|
|
20
30
|
# Specify the required pyinfra version (using PEP 440 setuptools specifier)
|
|
21
|
-
REQUIRE_PYINFRA_VERSION = None
|
|
31
|
+
REQUIRE_PYINFRA_VERSION: Optional[str] = None
|
|
22
32
|
# Specify any required packages (either using PEP 440 or a requirements file)
|
|
23
33
|
# Note: this can also include pyinfra potentially replacing REQUIRE_PYINFRA_VERSION
|
|
24
|
-
REQUIRE_PACKAGES = None
|
|
34
|
+
REQUIRE_PACKAGES: Optional[str] = None
|
|
25
35
|
# All these can be overridden inside individual operation calls:
|
|
26
36
|
# Switch to this user (from ssh_user) using su before executing operations
|
|
27
|
-
SU_USER = None
|
|
28
|
-
USE_SU_LOGIN = False
|
|
29
|
-
SU_SHELL =
|
|
30
|
-
PRESERVE_SU_ENV = False
|
|
37
|
+
SU_USER: Optional[str] = None
|
|
38
|
+
USE_SU_LOGIN: bool = False
|
|
39
|
+
SU_SHELL: bool = False
|
|
40
|
+
PRESERVE_SU_ENV: bool = False
|
|
31
41
|
# Use sudo and optional user
|
|
32
|
-
SUDO = False
|
|
33
|
-
SUDO_USER = None
|
|
34
|
-
PRESERVE_SUDO_ENV = False
|
|
35
|
-
USE_SUDO_LOGIN = False
|
|
36
|
-
|
|
42
|
+
SUDO: bool = False
|
|
43
|
+
SUDO_USER: Optional[str] = None
|
|
44
|
+
PRESERVE_SUDO_ENV: bool = False
|
|
45
|
+
USE_SUDO_LOGIN: bool = False
|
|
46
|
+
SUDO_PASSWORD: Optional[str] = None
|
|
37
47
|
# Use doas and optional user
|
|
38
|
-
DOAS = False
|
|
39
|
-
DOAS_USER = None
|
|
48
|
+
DOAS: bool = False
|
|
49
|
+
DOAS_USER: Optional[str] = None
|
|
40
50
|
# Only show errors but don't count as failure
|
|
41
|
-
IGNORE_ERRORS = False
|
|
51
|
+
IGNORE_ERRORS: bool = False
|
|
42
52
|
# Shell to use to execute commands
|
|
43
|
-
SHELL = "sh"
|
|
53
|
+
SHELL: str = "sh"
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items() if key.isupper()}
|
|
@@ -49,21 +59,113 @@ config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items()
|
|
|
49
59
|
def check_pyinfra_version(version: str):
|
|
50
60
|
if not version:
|
|
51
61
|
return
|
|
62
|
+
running_version = Version(__version__)
|
|
63
|
+
required_versions = SpecifierSet(version)
|
|
52
64
|
|
|
53
|
-
running_version
|
|
54
|
-
required_versions = Requirement.parse(
|
|
55
|
-
"pyinfra{0}".format(version),
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
if running_version not in required_versions: # type: ignore[operator]
|
|
65
|
+
if running_version not in required_versions:
|
|
59
66
|
raise PyinfraError(
|
|
60
|
-
|
|
61
|
-
version,
|
|
62
|
-
__version__,
|
|
63
|
-
),
|
|
67
|
+
f"pyinfra version requirement not met (requires {version}, running {__version__})"
|
|
64
68
|
)
|
|
65
69
|
|
|
66
70
|
|
|
71
|
+
def _check_requirements(requirements: Iterable[str]) -> Set[Requirement]:
|
|
72
|
+
"""
|
|
73
|
+
Check whether each of the given requirements and all their dependencies are
|
|
74
|
+
installed.
|
|
75
|
+
|
|
76
|
+
Or more precisely, this checks that each of the given *requirements* is
|
|
77
|
+
satisfied by some installed *distribution package*, and so on recursively
|
|
78
|
+
for each of the dependencies of those distribution packages. The terminology
|
|
79
|
+
here is as follows:
|
|
80
|
+
|
|
81
|
+
* A *distribution package* is essentially a thing that can be installed with
|
|
82
|
+
``pip``, from an sdist or wheel or Git repo or so on.
|
|
83
|
+
* A *requirement* is the expectation that a distribution package satisfying
|
|
84
|
+
some constraint is installed.
|
|
85
|
+
* A *dependency* is a requirement specified by a distribution package (as
|
|
86
|
+
opposed to the requirements passed in to this function).
|
|
87
|
+
|
|
88
|
+
So what this function does is start from the given requirements, for each
|
|
89
|
+
one check that it is satisfied by some installed distribution package, and
|
|
90
|
+
if so recursively perform the same check on all the dependencies of that
|
|
91
|
+
distribution package. In short, it's traversing the graph of package
|
|
92
|
+
requirements. It stops whenever it finds a requirement that is not satisfied
|
|
93
|
+
(i.e. a required package that is not installed), or when it runs out of
|
|
94
|
+
requirements to check.
|
|
95
|
+
|
|
96
|
+
.. note::
|
|
97
|
+
This is basically equivalent to ``pkg_resources.require()`` except that
|
|
98
|
+
when ``require()`` succeeds, it will return the list of distribution
|
|
99
|
+
packages that satisfy the given requirements and their dependencies, and
|
|
100
|
+
when it fails, it will raise an exception. This function just returns
|
|
101
|
+
the requirements which were not satisfied instead.
|
|
102
|
+
|
|
103
|
+
:param requirements: The requirements to check for in the set of installed
|
|
104
|
+
packages (along with their dependencies).
|
|
105
|
+
:return: The set of requirements that were not satisfied, which will be
|
|
106
|
+
an empty set if all requirements (recursively) were satisfied.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
# Based on pkg_resources.require() from setuptools. The implementation of
|
|
110
|
+
# hbutils.system.check_reqs() from the hbutils package was also helpful in
|
|
111
|
+
# clarifying what this is supposed to do.
|
|
112
|
+
|
|
113
|
+
reqs_to_check: Set[Requirement] = set(Requirement(r) for r in requirements)
|
|
114
|
+
reqs_satisfied: Set[Requirement] = set()
|
|
115
|
+
reqs_not_satisfied: Set[Requirement] = set()
|
|
116
|
+
|
|
117
|
+
while reqs_to_check:
|
|
118
|
+
req = reqs_to_check.pop()
|
|
119
|
+
assert req not in reqs_satisfied and req not in reqs_not_satisfied
|
|
120
|
+
|
|
121
|
+
# Check for an installed distribution package with the right name and version
|
|
122
|
+
try:
|
|
123
|
+
dist = importlib_metadata.distribution(req.name)
|
|
124
|
+
except importlib_metadata.PackageNotFoundError:
|
|
125
|
+
# No installed package with the right name
|
|
126
|
+
# This would raise a DistributionNotFound error from pkg_resources.require()
|
|
127
|
+
reqs_not_satisfied.add(req)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if dist.version not in req.specifier:
|
|
131
|
+
# There is a distribution with the right name but wrong version
|
|
132
|
+
# This would raise a VersionConflict error from pkg_resources.require()
|
|
133
|
+
reqs_not_satisfied.add(req)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
reqs_satisfied.add(req)
|
|
137
|
+
|
|
138
|
+
# If the distribution package has dependencies of its own, go through
|
|
139
|
+
# those dependencies and for each one add it to the set to be checked if
|
|
140
|
+
# - it's unconditional (no marker)
|
|
141
|
+
# - or it's conditional and the condition is satisfied (the marker
|
|
142
|
+
# evaluates to true) in the current environment
|
|
143
|
+
# Markers can check things like the Python version and system version
|
|
144
|
+
# etc., and/or they can check which extras of the distribution package
|
|
145
|
+
# were required. To facilitate checking extras we have to pass the extra
|
|
146
|
+
# in the environment when calling Marker.evaluate().
|
|
147
|
+
if dist.requires:
|
|
148
|
+
if req.extras:
|
|
149
|
+
extras_envs = [{"extra": extra} for extra in req.extras]
|
|
150
|
+
|
|
151
|
+
def evaluate_marker(marker: Marker) -> bool:
|
|
152
|
+
return any(map(marker.evaluate, extras_envs))
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
|
|
156
|
+
def evaluate_marker(marker: Marker) -> bool:
|
|
157
|
+
return marker.evaluate()
|
|
158
|
+
|
|
159
|
+
for dist_req_str in dist.requires:
|
|
160
|
+
dist_req = Requirement(dist_req_str)
|
|
161
|
+
if dist_req in reqs_satisfied or dist_req in reqs_not_satisfied:
|
|
162
|
+
continue
|
|
163
|
+
if (not dist_req.marker) or evaluate_marker(dist_req.marker):
|
|
164
|
+
reqs_to_check.add(dist_req)
|
|
165
|
+
|
|
166
|
+
return reqs_not_satisfied
|
|
167
|
+
|
|
168
|
+
|
|
67
169
|
def check_require_packages(requirements_config):
|
|
68
170
|
if not requirements_config:
|
|
69
171
|
return
|
|
@@ -74,14 +176,12 @@ def check_require_packages(requirements_config):
|
|
|
74
176
|
with open(path.join(state.cwd or "", requirements_config), encoding="utf-8") as f:
|
|
75
177
|
requirements = [line.split("#egg=")[-1] for line in f.read().splitlines()]
|
|
76
178
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
except ResolutionError as e:
|
|
179
|
+
requirements_not_met = _check_requirements(requirements)
|
|
180
|
+
if requirements_not_met:
|
|
80
181
|
raise PyinfraError(
|
|
81
|
-
"Deploy requirements ({0}) not met: {1}".format(
|
|
82
|
-
requirements_config,
|
|
83
|
-
|
|
84
|
-
),
|
|
182
|
+
"Deploy requirements ({0}) not met: missing {1}".format(
|
|
183
|
+
requirements_config, ", ".join(str(r) for r in requirements_not_met)
|
|
184
|
+
)
|
|
85
185
|
)
|
|
86
186
|
|
|
87
187
|
|
pyinfra/api/connectors.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
try:
|
|
2
|
+
from importlib_metadata import entry_points
|
|
3
|
+
except ImportError:
|
|
4
|
+
from importlib.metadata import entry_points # type: ignore[assignment]
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
def _load_connector(entrypoint):
|
|
@@ -8,7 +11,7 @@ def _load_connector(entrypoint):
|
|
|
8
11
|
def get_all_connectors():
|
|
9
12
|
return {
|
|
10
13
|
entrypoint.name: _load_connector(entrypoint)
|
|
11
|
-
for entrypoint in
|
|
14
|
+
for entrypoint in entry_points(group="pyinfra.connectors")
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
|
pyinfra/api/deploy.py
CHANGED
|
@@ -5,13 +5,16 @@ creation (eg pyinfra-openstack).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from functools import wraps
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, cast
|
|
9
|
+
|
|
10
|
+
from typing_extensions import ParamSpec
|
|
9
11
|
|
|
10
12
|
import pyinfra
|
|
11
13
|
from pyinfra import context
|
|
12
14
|
from pyinfra.context import ctx_host, ctx_state
|
|
13
15
|
|
|
14
16
|
from .arguments import pop_global_arguments
|
|
17
|
+
from .arguments_typed import PyinfraOperation
|
|
15
18
|
from .exceptions import PyinfraError
|
|
16
19
|
from .host import Host
|
|
17
20
|
from .util import get_call_location
|
|
@@ -48,40 +51,37 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs)
|
|
|
48
51
|
deploy_func(*args, **kwargs)
|
|
49
52
|
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
P = ParamSpec("P")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def deploy(name: Optional[str] = None, data_defaults=None):
|
|
52
58
|
"""
|
|
53
59
|
Decorator that takes a deploy function (normally from a pyinfra_* package)
|
|
54
60
|
and wraps any operations called inside with any deploy-wide kwargs/data.
|
|
55
61
|
"""
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
f.deploy_name = name
|
|
63
|
-
if data_defaults:
|
|
64
|
-
f.deploy_data = data_defaults
|
|
65
|
-
return deploy(f, _call_location=get_call_location())
|
|
63
|
+
def decorator(func: Callable[P, Any]) -> PyinfraOperation[P]:
|
|
64
|
+
func.deploy_name = name or func.__name__ # type: ignore[attr-defined]
|
|
65
|
+
if data_defaults:
|
|
66
|
+
func.deploy_data = data_defaults # type: ignore[attr-defined]
|
|
67
|
+
return _wrap_deploy(func)
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
return decorator
|
|
68
70
|
|
|
69
|
-
# Actually decorate!
|
|
70
|
-
func = func_or_name
|
|
71
71
|
|
|
72
|
+
def _wrap_deploy(func: Callable[P, Any]) -> PyinfraOperation[P]:
|
|
72
73
|
@wraps(func)
|
|
73
|
-
def decorated_func(*args, **kwargs):
|
|
74
|
+
def decorated_func(*args: P.args, **kwargs: P.kwargs) -> Any:
|
|
74
75
|
deploy_kwargs, _ = pop_global_arguments(kwargs)
|
|
75
76
|
|
|
76
|
-
# Name the deploy
|
|
77
|
-
deploy_name = getattr(func, "deploy_name", func.__name__)
|
|
78
77
|
deploy_data = getattr(func, "deploy_data", None)
|
|
79
78
|
|
|
80
79
|
with context.host.deploy(
|
|
81
|
-
name=deploy_name,
|
|
80
|
+
name=func.deploy_name, # type: ignore[attr-defined]
|
|
82
81
|
kwargs=deploy_kwargs,
|
|
83
82
|
data=deploy_data,
|
|
84
83
|
):
|
|
85
84
|
return func(*args, **kwargs)
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
decorated_func._inner = func # type: ignore[attr-defined]
|
|
87
|
+
return cast(PyinfraOperation[P], decorated_func)
|
pyinfra/api/exceptions.py
CHANGED
|
@@ -10,6 +10,25 @@ class ConnectError(PyinfraError):
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class FactError(PyinfraError):
|
|
14
|
+
"""
|
|
15
|
+
Exception raised during fact gathering staging if a fact is unable to
|
|
16
|
+
generate output/change state.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FactTypeError(FactError, TypeError):
|
|
21
|
+
"""
|
|
22
|
+
Exception raised when a fact is passed invalid argument types.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FactValueError(FactError, ValueError):
|
|
27
|
+
"""
|
|
28
|
+
Exception raised when a fact is passed invalid argument values.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
13
32
|
class OperationError(PyinfraError):
|
|
14
33
|
"""
|
|
15
34
|
Exception raised during fact gathering staging if an operation is unable to
|
|
@@ -41,19 +60,31 @@ class InventoryError(PyinfraError):
|
|
|
41
60
|
"""
|
|
42
61
|
|
|
43
62
|
|
|
44
|
-
class NoConnectorError(PyinfraError,
|
|
63
|
+
class NoConnectorError(PyinfraError, ValueError):
|
|
45
64
|
"""
|
|
46
65
|
Raised when a requested connector is missing.
|
|
47
66
|
"""
|
|
48
67
|
|
|
49
68
|
|
|
50
|
-
class NoHostError(PyinfraError,
|
|
69
|
+
class NoHostError(PyinfraError, KeyError):
|
|
51
70
|
"""
|
|
52
71
|
Raised when an inventory is missing a host.
|
|
53
72
|
"""
|
|
54
73
|
|
|
55
74
|
|
|
56
|
-
class NoGroupError(PyinfraError,
|
|
75
|
+
class NoGroupError(PyinfraError, KeyError):
|
|
76
|
+
"""
|
|
77
|
+
Raised when an inventory is missing a group.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ConnectorDataTypeError(PyinfraError, TypeError):
|
|
82
|
+
"""
|
|
83
|
+
Raised when host connector data has invalid types.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ArgumentTypeError(PyinfraError, TypeError):
|
|
57
88
|
"""
|
|
58
|
-
|
|
89
|
+
Raised when global arguments are passed with invalid types.
|
|
59
90
|
"""
|
pyinfra/api/facts.py
CHANGED
|
@@ -10,10 +10,11 @@ other host B while I operate on this host A).
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import inspect
|
|
13
14
|
import re
|
|
14
15
|
from inspect import getcallargs
|
|
15
16
|
from socket import error as socket_error, timeout as timeout_error
|
|
16
|
-
from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional,
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Optional, Type, TypeVar, cast
|
|
17
18
|
|
|
18
19
|
import click
|
|
19
20
|
import gevent
|
|
@@ -21,68 +22,87 @@ from paramiko import SSHException
|
|
|
21
22
|
|
|
22
23
|
from pyinfra import logger
|
|
23
24
|
from pyinfra.api import StringCommand
|
|
24
|
-
from pyinfra.api.arguments import pop_global_arguments
|
|
25
|
+
from pyinfra.api.arguments import all_global_arguments, pop_global_arguments
|
|
25
26
|
from pyinfra.api.util import (
|
|
26
27
|
get_kwargs_str,
|
|
27
28
|
log_error_or_warning,
|
|
28
29
|
log_host_command_error,
|
|
29
|
-
make_hash,
|
|
30
30
|
print_host_combined_output,
|
|
31
31
|
)
|
|
32
32
|
from pyinfra.connectors.util import CommandOutput
|
|
33
33
|
from pyinfra.context import ctx_host, ctx_state
|
|
34
34
|
from pyinfra.progress import progress_spinner
|
|
35
35
|
|
|
36
|
-
from .arguments import
|
|
36
|
+
from .arguments import CONNECTOR_ARGUMENT_KEYS
|
|
37
37
|
|
|
38
38
|
if TYPE_CHECKING:
|
|
39
39
|
from pyinfra.api.host import Host
|
|
40
40
|
from pyinfra.api.state import State
|
|
41
41
|
|
|
42
|
-
SUDO_REGEX = r"^sudo: unknown user
|
|
42
|
+
SUDO_REGEX = r"^sudo: unknown user"
|
|
43
43
|
SU_REGEXES = (
|
|
44
44
|
r"^su: user .+ does not exist",
|
|
45
45
|
r"^su: unknown login",
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
def __init__(cls, name: str, bases, attrs, **kwargs):
|
|
51
|
-
super().__init__(name, bases, attrs, **kwargs)
|
|
52
|
-
module_name = cls.__module__.replace("pyinfra.facts.", "")
|
|
53
|
-
cls.name = f"{module_name}.{cls.__name__}"
|
|
49
|
+
T = TypeVar("T")
|
|
54
50
|
|
|
55
51
|
|
|
56
|
-
class FactBase(
|
|
52
|
+
class FactBase(Generic[T]):
|
|
57
53
|
name: str
|
|
58
54
|
|
|
59
55
|
abstract: bool = True
|
|
60
56
|
|
|
61
|
-
shell_executable:
|
|
57
|
+
shell_executable: str | None = None
|
|
58
|
+
|
|
59
|
+
command: Callable[..., str | StringCommand]
|
|
62
60
|
|
|
63
|
-
requires_command
|
|
61
|
+
def requires_command(self, *args, **kwargs) -> str | None:
|
|
62
|
+
return None
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
def __init_subclass__(cls) -> None:
|
|
65
|
+
super().__init_subclass__()
|
|
66
|
+
module_name = cls.__module__.replace("pyinfra.facts.", "")
|
|
67
|
+
cls.name = f"{module_name}.{cls.__name__}"
|
|
68
|
+
|
|
69
|
+
# Check that fact's `command` method does not inadvertently take a global
|
|
70
|
+
# argument, most commonly `name`.
|
|
71
|
+
if hasattr(cls, "command") and callable(cls.command):
|
|
72
|
+
command_args = set(inspect.signature(cls.command).parameters.keys())
|
|
73
|
+
global_args = set([name for name, _ in all_global_arguments()])
|
|
74
|
+
command_global_args = command_args & global_args
|
|
75
|
+
|
|
76
|
+
if len(command_global_args) > 0:
|
|
77
|
+
names = ", ".join(command_global_args)
|
|
78
|
+
raise TypeError(f"{cls.name}'s arguments {names} are reserved for global arguments")
|
|
66
79
|
|
|
67
80
|
@staticmethod
|
|
68
|
-
def default():
|
|
81
|
+
def default() -> T:
|
|
69
82
|
"""
|
|
70
83
|
Set the default attribute to be a type (eg list/dict).
|
|
71
84
|
"""
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
return cast(T, None)
|
|
87
|
+
|
|
88
|
+
def process(self, output: Iterable[str]) -> T:
|
|
89
|
+
# NOTE: TypeVar does not support a default, so we have to cast this str -> T
|
|
90
|
+
return cast(T, "\n".join(output))
|
|
76
91
|
|
|
77
92
|
def process_pipeline(self, args, output):
|
|
78
93
|
return {arg: self.process([output[i]]) for i, arg in enumerate(args)}
|
|
79
94
|
|
|
80
95
|
|
|
81
|
-
class ShortFactBase(
|
|
82
|
-
|
|
96
|
+
class ShortFactBase(Generic[T]):
|
|
97
|
+
name: str
|
|
98
|
+
fact: Type[FactBase]
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
def __init_subclass__(cls) -> None:
|
|
101
|
+
super().__init_subclass__()
|
|
102
|
+
module_name = cls.__module__.replace("pyinfra.facts.", "")
|
|
103
|
+
cls.name = f"{module_name}.{cls.__name__}"
|
|
104
|
+
|
|
105
|
+
def process_data(self, data):
|
|
86
106
|
return data
|
|
87
107
|
|
|
88
108
|
|
|
@@ -98,64 +118,29 @@ def _make_command(command_attribute, host_args):
|
|
|
98
118
|
return command_attribute
|
|
99
119
|
|
|
100
120
|
|
|
101
|
-
def _get_executor_kwargs(
|
|
102
|
-
state: "State",
|
|
103
|
-
host: "Host",
|
|
104
|
-
override_kwargs: Optional[dict[str, Any]] = None,
|
|
105
|
-
override_kwarg_keys: Optional[list[str]] = None,
|
|
106
|
-
):
|
|
107
|
-
if override_kwargs is None:
|
|
108
|
-
override_kwargs = {}
|
|
109
|
-
if override_kwarg_keys is None:
|
|
110
|
-
override_kwarg_keys = []
|
|
111
|
-
|
|
112
|
-
# Use the current operation global kwargs, or generate defaults
|
|
113
|
-
global_kwargs = host.current_op_global_arguments
|
|
114
|
-
if not global_kwargs:
|
|
115
|
-
global_kwargs, _ = pop_global_arguments({}, state, host)
|
|
116
|
-
|
|
117
|
-
# Apply any current op kwargs that *weren't* found in the overrides
|
|
118
|
-
override_kwargs.update(
|
|
119
|
-
{key: value for key, value in global_kwargs.items() if key not in override_kwarg_keys},
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
key: value for key, value in override_kwargs.items() if key in get_connector_argument_keys()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
121
|
def _handle_fact_kwargs(state, host, cls, args, kwargs):
|
|
128
122
|
args = args or []
|
|
129
123
|
kwargs = kwargs or {}
|
|
130
124
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
# Start with a (shallow) copy of current operation kwargs if any
|
|
126
|
+
ctx_kwargs = (host.current_op_global_arguments or {}).copy()
|
|
127
|
+
# Update with the input kwargs (overrides)
|
|
128
|
+
ctx_kwargs.update(kwargs)
|
|
134
129
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
kwargs,
|
|
130
|
+
# Pop executor kwargs, pass remaining
|
|
131
|
+
global_kwargs, _ = pop_global_arguments(
|
|
132
|
+
ctx_kwargs,
|
|
139
133
|
state=state,
|
|
140
134
|
host=host,
|
|
141
|
-
keys_to_check=get_connector_argument_keys(),
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
executor_kwargs = _get_executor_kwargs(
|
|
145
|
-
state,
|
|
146
|
-
host,
|
|
147
|
-
override_kwargs=override_kwargs, # type: ignore[arg-type]
|
|
148
|
-
override_kwarg_keys=override_kwarg_keys,
|
|
149
135
|
)
|
|
150
136
|
|
|
151
|
-
fact_kwargs = {}
|
|
137
|
+
fact_kwargs = {key: value for key, value in kwargs.items() if key not in global_kwargs}
|
|
152
138
|
|
|
153
|
-
if args or
|
|
154
|
-
assert not isinstance(cls.command, str)
|
|
139
|
+
if args or fact_kwargs:
|
|
155
140
|
# Merges args & kwargs into a single kwargs dictionary
|
|
156
|
-
fact_kwargs = getcallargs(cls().command, *args, **
|
|
141
|
+
fact_kwargs = getcallargs(cls().command, *args, **fact_kwargs)
|
|
157
142
|
|
|
158
|
-
return fact_kwargs,
|
|
143
|
+
return fact_kwargs, global_kwargs
|
|
159
144
|
|
|
160
145
|
|
|
161
146
|
def get_facts(state: "State", *args, **kwargs):
|
|
@@ -223,7 +208,7 @@ def _get_fact(
|
|
|
223
208
|
fact = cls()
|
|
224
209
|
name = fact.name
|
|
225
210
|
|
|
226
|
-
fact_kwargs,
|
|
211
|
+
fact_kwargs, global_kwargs = _handle_fact_kwargs(state, host, cls, args, kwargs)
|
|
227
212
|
|
|
228
213
|
kwargs_str = get_kwargs_str(fact_kwargs)
|
|
229
214
|
logger.debug(
|
|
@@ -239,15 +224,9 @@ def _get_fact(
|
|
|
239
224
|
raise_exceptions=True,
|
|
240
225
|
)
|
|
241
226
|
|
|
242
|
-
ignore_errors = (
|
|
243
|
-
host.current_op_global_arguments["_ignore_errors"]
|
|
244
|
-
if host.in_op and host.current_op_global_arguments
|
|
245
|
-
else state.config.IGNORE_ERRORS
|
|
246
|
-
)
|
|
247
|
-
|
|
248
227
|
# Facts can override the shell (winrm powershell vs cmd support)
|
|
249
228
|
if fact.shell_executable:
|
|
250
|
-
|
|
229
|
+
global_kwargs["_shell_executable"] = fact.shell_executable
|
|
251
230
|
|
|
252
231
|
command = _make_command(fact.command, fact_kwargs)
|
|
253
232
|
requires_command = _make_command(fact.requires_command, fact_kwargs)
|
|
@@ -266,6 +245,10 @@ def _get_fact(
|
|
|
266
245
|
status = False
|
|
267
246
|
output = CommandOutput([])
|
|
268
247
|
|
|
248
|
+
executor_kwargs = {
|
|
249
|
+
key: value for key, value in global_kwargs.items() if key in CONNECTOR_ARGUMENT_KEYS
|
|
250
|
+
}
|
|
251
|
+
|
|
269
252
|
try:
|
|
270
253
|
status, output = host.run_shell_command(
|
|
271
254
|
command,
|
|
@@ -277,7 +260,7 @@ def _get_fact(
|
|
|
277
260
|
log_host_command_error(
|
|
278
261
|
host,
|
|
279
262
|
e,
|
|
280
|
-
timeout=
|
|
263
|
+
timeout=global_kwargs.get("_timeout"),
|
|
281
264
|
)
|
|
282
265
|
|
|
283
266
|
stdout_lines, stderr_lines = output.stdout_lines, output.stderr_lines
|
|
@@ -316,24 +299,17 @@ def _get_fact(
|
|
|
316
299
|
|
|
317
300
|
log_error_or_warning(
|
|
318
301
|
host,
|
|
319
|
-
|
|
302
|
+
global_kwargs["_ignore_errors"],
|
|
320
303
|
description=("could not load fact: {0} {1}").format(name, get_kwargs_str(fact_kwargs)),
|
|
321
304
|
)
|
|
322
305
|
|
|
323
306
|
# Check we've not failed
|
|
324
|
-
if not status and not
|
|
307
|
+
if apply_failed_hosts and not status and not global_kwargs["_ignore_errors"]:
|
|
325
308
|
state.fail_hosts({host})
|
|
326
309
|
|
|
327
310
|
return data
|
|
328
311
|
|
|
329
312
|
|
|
330
|
-
def _get_fact_hash(state: "State", host: "Host", cls, args, kwargs):
|
|
331
|
-
if issubclass(cls, ShortFactBase):
|
|
332
|
-
cls = cls.fact
|
|
333
|
-
fact_kwargs, executor_kwargs = _handle_fact_kwargs(state, host, cls, args, kwargs)
|
|
334
|
-
return make_hash((cls, fact_kwargs, executor_kwargs))
|
|
335
|
-
|
|
336
|
-
|
|
337
313
|
def get_host_fact(
|
|
338
314
|
state: "State",
|
|
339
315
|
host: "Host",
|