pyinfra 2.9.2__py2.py3-none-any.whl → 3.0__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 +265 -253
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +68 -53
- pyinfra/api/config.py +139 -32
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +7 -26
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +102 -137
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +21 -25
- pyinfra/api/operation.py +240 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +79 -86
- pyinfra/connectors/base.py +147 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +220 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +196 -208
- pyinfra/connectors/ssh.py +530 -613
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +211 -137
- pyinfra/connectors/vagrant.py +60 -53
- pyinfra/context.py +4 -2
- pyinfra/facts/apk.py +2 -0
- pyinfra/facts/apt.py +2 -0
- pyinfra/facts/brew.py +2 -0
- pyinfra/facts/bsdinit.py +2 -0
- pyinfra/facts/cargo.py +2 -0
- pyinfra/facts/choco.py +2 -0
- pyinfra/facts/deb.py +7 -2
- pyinfra/facts/dnf.py +2 -0
- pyinfra/facts/docker.py +19 -0
- pyinfra/facts/files.py +47 -32
- pyinfra/facts/gem.py +2 -0
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +3 -1
- pyinfra/facts/hardware.py +34 -24
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/launchd.py +2 -0
- pyinfra/facts/lxd.py +2 -0
- pyinfra/facts/mysql.py +13 -6
- pyinfra/facts/npm.py +1 -0
- pyinfra/facts/openrc.py +2 -0
- pyinfra/facts/pacman.py +6 -2
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +2 -0
- pyinfra/facts/pkgin.py +2 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +6 -160
- pyinfra/facts/rpm.py +12 -9
- pyinfra/facts/runit.py +68 -0
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +80 -36
- pyinfra/facts/snap.py +2 -0
- pyinfra/facts/systemd.py +31 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/upstart.py +2 -0
- pyinfra/facts/util/packaging.py +7 -4
- pyinfra/facts/vzctl.py +2 -0
- pyinfra/facts/xbps.py +2 -0
- pyinfra/facts/yum.py +2 -0
- pyinfra/facts/zypper.py +2 -0
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +6 -4
- pyinfra/operations/apt.py +46 -65
- pyinfra/operations/brew.py +17 -22
- pyinfra/operations/bsdinit.py +9 -7
- pyinfra/operations/cargo.py +4 -2
- pyinfra/operations/choco.py +4 -2
- pyinfra/operations/dnf.py +19 -23
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +188 -386
- pyinfra/operations/gem.py +4 -2
- pyinfra/operations/git.py +24 -53
- pyinfra/operations/iptables.py +29 -35
- pyinfra/operations/launchd.py +6 -7
- pyinfra/operations/lxd.py +8 -13
- pyinfra/operations/mysql.py +62 -81
- pyinfra/operations/npm.py +9 -2
- pyinfra/operations/openrc.py +6 -4
- pyinfra/operations/pacman.py +7 -8
- pyinfra/operations/pip.py +25 -24
- pyinfra/operations/pkg.py +4 -2
- pyinfra/operations/pkgin.py +6 -4
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -379
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -44
- pyinfra/operations/server.py +111 -127
- pyinfra/operations/snap.py +4 -4
- pyinfra/operations/ssh.py +20 -33
- pyinfra/operations/systemd.py +19 -15
- pyinfra/operations/sysvinit.py +9 -16
- pyinfra/operations/upstart.py +9 -7
- 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 +55 -57
- pyinfra/operations/util/service.py +39 -51
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +6 -4
- pyinfra/operations/yum.py +18 -22
- pyinfra/operations/zypper.py +12 -13
- pyinfra/version.py +5 -2
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
- pyinfra-3.0.dist-info/RECORD +167 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
- pyinfra-3.0.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +78 -42
- pyinfra_cli/inventory.py +40 -6
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +95 -127
- pyinfra_cli/util.py +62 -29
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +13 -13
- tests/test_api/test_api_deploys.py +28 -29
- tests/test_api/test_api_facts.py +60 -98
- tests/test_api/test_api_operations.py +101 -201
- tests/test_cli/test_cli.py +18 -49
- tests/test_cli/test_cli_deploy.py +11 -37
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_chroot.py +6 -6
- tests/test_connectors/test_docker.py +4 -4
- tests/test_connectors/test_dockerssh.py +38 -50
- tests/test_connectors/test_local.py +11 -12
- tests/test_connectors/test_ssh.py +105 -93
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +7 -7
- pyinfra/api/operation.pyi +0 -117
- pyinfra/connectors/ansible.py +0 -171
- pyinfra/connectors/mech.py +0 -186
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -320
- 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 -551
- pyinfra-2.9.2.dist-info/RECORD +0 -170
- pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- tests/test_connectors/test_winrm.py +0 -76
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
pyinfra/api/host.py
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from contextlib import contextmanager
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
TYPE_CHECKING,
|
|
6
|
+
Any,
|
|
7
|
+
Callable,
|
|
8
|
+
Generator,
|
|
9
|
+
Optional,
|
|
10
|
+
Type,
|
|
11
|
+
TypeVar,
|
|
12
|
+
Union,
|
|
13
|
+
cast,
|
|
14
|
+
overload,
|
|
15
|
+
)
|
|
16
|
+
from uuid import uuid4
|
|
3
17
|
|
|
4
18
|
import click
|
|
5
|
-
from
|
|
19
|
+
from typing_extensions import Unpack
|
|
6
20
|
|
|
7
21
|
from pyinfra import logger
|
|
8
|
-
from pyinfra.connectors.
|
|
22
|
+
from pyinfra.connectors.base import BaseConnector
|
|
23
|
+
from pyinfra.connectors.util import CommandOutput, remove_any_sudo_askpass_file
|
|
9
24
|
|
|
10
25
|
from .connectors import get_execution_connector
|
|
11
26
|
from .exceptions import ConnectError
|
|
12
|
-
from .facts import
|
|
27
|
+
from .facts import FactBase, ShortFactBase, get_host_fact
|
|
28
|
+
from .util import memoize, sha1_hash
|
|
13
29
|
|
|
14
30
|
if TYPE_CHECKING:
|
|
31
|
+
from pyinfra.api.arguments import AllArguments
|
|
15
32
|
from pyinfra.api.inventory import Inventory
|
|
16
33
|
from pyinfra.api.state import State
|
|
17
34
|
|
|
18
35
|
|
|
19
|
-
def extract_callable_datas(
|
|
36
|
+
def extract_callable_datas(
|
|
37
|
+
datas: list[Union[Callable[..., Any], Any]],
|
|
38
|
+
) -> Generator[Any, Any, Any]:
|
|
20
39
|
for data in datas:
|
|
21
40
|
# Support for dynamic data, ie @deploy wrapped data defaults where
|
|
22
41
|
# the data is stored on the state temporarily.
|
|
@@ -31,7 +50,7 @@ class HostData:
|
|
|
31
50
|
Combines multiple AttrData's to search for attributes.
|
|
32
51
|
"""
|
|
33
52
|
|
|
34
|
-
override_datas:
|
|
53
|
+
override_datas: dict[str, Any]
|
|
35
54
|
|
|
36
55
|
def __init__(self, host: "Host", *datas):
|
|
37
56
|
self.__dict__["host"] = host
|
|
@@ -82,28 +101,36 @@ class Host:
|
|
|
82
101
|
data.
|
|
83
102
|
"""
|
|
84
103
|
|
|
85
|
-
connection = None
|
|
86
104
|
state: "State"
|
|
105
|
+
connector_cls: type[BaseConnector]
|
|
106
|
+
connector: BaseConnector
|
|
107
|
+
connected: bool = False
|
|
87
108
|
|
|
88
109
|
# Current context inside an @operation function (op gen stage)
|
|
89
110
|
in_op: bool = False
|
|
111
|
+
in_callback_op: bool = False
|
|
90
112
|
current_op_hash: Optional[str] = None
|
|
91
|
-
|
|
113
|
+
current_op_global_arguments: Optional["AllArguments"] = None
|
|
92
114
|
|
|
93
|
-
# Current context inside a @deploy function
|
|
115
|
+
# Current context inside a @deploy function which become part of the op data
|
|
94
116
|
in_deploy: bool = False
|
|
95
117
|
current_deploy_name: Optional[str] = None
|
|
96
118
|
current_deploy_kwargs = None
|
|
97
|
-
|
|
119
|
+
|
|
120
|
+
# @deploy decorator data is a bit different - we need to handle the case
|
|
121
|
+
# where we're evaluating an operation at runtime (current_op_) but also
|
|
122
|
+
# when ordering operations (current_) outside of an operation context.
|
|
123
|
+
current_op_deploy_data: Optional[dict[str, Any]] = None
|
|
124
|
+
current_deploy_data: Optional[dict[str, Any]] = None
|
|
98
125
|
|
|
99
126
|
# Current context during operation execution
|
|
100
|
-
executing_op_hash = None
|
|
101
|
-
nested_executing_op_hash = None
|
|
127
|
+
executing_op_hash: Optional[str] = None
|
|
128
|
+
nested_executing_op_hash: Optional[str] = None
|
|
102
129
|
|
|
103
|
-
loop_position:
|
|
130
|
+
loop_position: list[int]
|
|
104
131
|
|
|
105
132
|
# Arbitrary data dictionary connectors may use
|
|
106
|
-
connector_data:
|
|
133
|
+
connector_data: dict[str, Any]
|
|
107
134
|
|
|
108
135
|
def loop(self, iterable):
|
|
109
136
|
self.loop_position.append(0)
|
|
@@ -117,26 +144,20 @@ class Host:
|
|
|
117
144
|
name: str,
|
|
118
145
|
inventory: "Inventory",
|
|
119
146
|
groups,
|
|
120
|
-
|
|
147
|
+
connector_cls=get_execution_connector("ssh"),
|
|
121
148
|
):
|
|
122
149
|
self.inventory = inventory
|
|
123
150
|
self.groups = groups
|
|
124
|
-
self.
|
|
151
|
+
self.connector_cls = connector_cls
|
|
125
152
|
self.name = name
|
|
126
153
|
|
|
127
154
|
self.loop_position = []
|
|
128
155
|
|
|
129
156
|
self.connector_data = {}
|
|
130
|
-
self.current_op_global_kwargs = {}
|
|
131
|
-
|
|
132
|
-
# Fact data store
|
|
133
|
-
# TODO: how to not have Any here?
|
|
134
|
-
self.facts: Dict[str, Any] = {}
|
|
135
|
-
self.facts_lock = BoundedSemaphore()
|
|
136
157
|
|
|
137
158
|
# Append only list of operation hashes as called on this host, used to
|
|
138
159
|
# generate a DAG to create the final operation order.
|
|
139
|
-
self.op_hash_order:
|
|
160
|
+
self.op_hash_order: list[str] = []
|
|
140
161
|
|
|
141
162
|
# Create the (waterfall data: override, host, group, global)
|
|
142
163
|
self.data = HostData(
|
|
@@ -149,16 +170,20 @@ class Host:
|
|
|
149
170
|
self.get_deploy_data,
|
|
150
171
|
)
|
|
151
172
|
|
|
173
|
+
def init(self, state: "State") -> None:
|
|
174
|
+
self.state = state
|
|
175
|
+
self.connector = self.connector_cls(state, self)
|
|
176
|
+
|
|
177
|
+
longest_name_len = max([len(host.name) for host in self.inventory])
|
|
178
|
+
padding_diff = longest_name_len - len(self.name)
|
|
179
|
+
self.print_prefix_padding = "".join(" " for _ in range(0, padding_diff))
|
|
180
|
+
|
|
152
181
|
def __str__(self):
|
|
153
182
|
return "{0}".format(self.name)
|
|
154
183
|
|
|
155
184
|
def __repr__(self):
|
|
156
185
|
return "Host({0})".format(self.name)
|
|
157
186
|
|
|
158
|
-
@property
|
|
159
|
-
def connected(self) -> bool:
|
|
160
|
-
return self.connection is not None
|
|
161
|
-
|
|
162
187
|
@property
|
|
163
188
|
def host_data(self):
|
|
164
189
|
return self.inventory.get_host_data(self.name)
|
|
@@ -168,30 +193,37 @@ class Host:
|
|
|
168
193
|
return self.inventory.get_groups_data(self.groups)
|
|
169
194
|
|
|
170
195
|
@property
|
|
171
|
-
def print_prefix(self):
|
|
196
|
+
def print_prefix(self) -> str:
|
|
172
197
|
if self.nested_executing_op_hash:
|
|
173
|
-
return "{0}[{1}] {2} ".format(
|
|
198
|
+
return "{0}[{1}] {2}{3} ".format(
|
|
174
199
|
click.style(""), # reset
|
|
175
200
|
click.style(self.name, bold=True),
|
|
176
201
|
click.style("nested", "blue"),
|
|
202
|
+
self.print_prefix_padding,
|
|
177
203
|
)
|
|
178
204
|
|
|
179
|
-
return "{0}[{1}] ".format(
|
|
205
|
+
return "{0}[{1}]{2} ".format(
|
|
180
206
|
click.style(""), # reset
|
|
181
207
|
click.style(self.name, bold=True),
|
|
208
|
+
self.print_prefix_padding,
|
|
182
209
|
)
|
|
183
210
|
|
|
184
|
-
def style_print_prefix(self, *args, **kwargs):
|
|
185
|
-
return "{0}[{1}] ".format(
|
|
211
|
+
def style_print_prefix(self, *args, **kwargs) -> str:
|
|
212
|
+
return "{0}[{1}]{2} ".format(
|
|
186
213
|
click.style(""), # reset
|
|
187
214
|
click.style(self.name, *args, **kwargs),
|
|
215
|
+
self.print_prefix_padding,
|
|
188
216
|
)
|
|
189
217
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
def log(self, message, log_func=logger.info):
|
|
219
|
+
log_func(f"{self.print_prefix}{message}")
|
|
220
|
+
|
|
221
|
+
def log_styled(self, message, log_func=logger.info, **kwargs):
|
|
222
|
+
message_styled = click.style(message, **kwargs)
|
|
223
|
+
self.log(message_styled, log_func=log_func)
|
|
193
224
|
|
|
194
|
-
|
|
225
|
+
def get_deploy_data(self):
|
|
226
|
+
return self.current_op_deploy_data or self.current_deploy_data or {}
|
|
195
227
|
|
|
196
228
|
def noop(self, description):
|
|
197
229
|
"""
|
|
@@ -201,8 +233,25 @@ class Host:
|
|
|
201
233
|
handler = logger.info if self.state.print_noop_info else logger.debug
|
|
202
234
|
handler("{0}noop: {1}".format(self.print_prefix, description))
|
|
203
235
|
|
|
236
|
+
def when(self, condition: Callable[[], bool]):
|
|
237
|
+
return self.deploy(
|
|
238
|
+
"",
|
|
239
|
+
cast("AllArguments", {"_if": [condition]}),
|
|
240
|
+
{},
|
|
241
|
+
in_deploy=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def arguments(self, **arguments: Unpack["AllArguments"]):
|
|
245
|
+
return self.deploy("", arguments, {}, in_deploy=False)
|
|
246
|
+
|
|
204
247
|
@contextmanager
|
|
205
|
-
def deploy(
|
|
248
|
+
def deploy(
|
|
249
|
+
self,
|
|
250
|
+
name: str,
|
|
251
|
+
kwargs: Optional["AllArguments"],
|
|
252
|
+
data: Optional[dict],
|
|
253
|
+
in_deploy: bool = True,
|
|
254
|
+
):
|
|
206
255
|
"""
|
|
207
256
|
Wraps a group of operations as a deploy, this should not be used
|
|
208
257
|
directly, instead use ``pyinfra.api.deploy.deploy``.
|
|
@@ -219,6 +268,13 @@ class Host:
|
|
|
219
268
|
old_deploy_data = self.current_deploy_data
|
|
220
269
|
self.in_deploy = in_deploy
|
|
221
270
|
|
|
271
|
+
# Combine any old _ifs with the new ones
|
|
272
|
+
if old_deploy_kwargs and kwargs:
|
|
273
|
+
old_ifs = old_deploy_kwargs["_if"]
|
|
274
|
+
new_ifs = kwargs["_if"]
|
|
275
|
+
if old_ifs and new_ifs:
|
|
276
|
+
kwargs["_if"] = old_ifs + new_ifs
|
|
277
|
+
|
|
222
278
|
# Set the new values
|
|
223
279
|
self.current_deploy_name = name
|
|
224
280
|
self.current_deploy_kwargs = kwargs
|
|
@@ -245,33 +301,53 @@ class Host:
|
|
|
245
301
|
old_deploy_data,
|
|
246
302
|
)
|
|
247
303
|
|
|
248
|
-
|
|
249
|
-
|
|
304
|
+
@memoize
|
|
305
|
+
def _get_temp_directory(self):
|
|
306
|
+
temp_directory = self.state.config.TEMP_DIR
|
|
250
307
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return get_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
|
|
308
|
+
if temp_directory is None:
|
|
309
|
+
# Unfortunate, but very hard to avoid, circular dependency, this method is memoized so
|
|
310
|
+
# performance isn't a concern.
|
|
311
|
+
from pyinfra.facts.server import TmpDir
|
|
256
312
|
|
|
257
|
-
|
|
258
|
-
"""
|
|
259
|
-
Get a fact for this host without using any cached value, always re-fetch the fact data
|
|
260
|
-
from the host and then cache it.
|
|
261
|
-
"""
|
|
262
|
-
return reload_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
|
|
313
|
+
temp_directory = self.get_fact(TmpDir)
|
|
263
314
|
|
|
264
|
-
|
|
315
|
+
if not temp_directory:
|
|
316
|
+
temp_directory = self.state.config.DEFAULT_TEMP_DIR
|
|
317
|
+
|
|
318
|
+
return temp_directory
|
|
319
|
+
|
|
320
|
+
def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
|
|
265
321
|
"""
|
|
266
|
-
|
|
322
|
+
Generate a temporary filename for this deploy.
|
|
267
323
|
"""
|
|
268
|
-
return create_host_fact(self.state, self, name_or_cls, data, kwargs=kwargs)
|
|
269
324
|
|
|
270
|
-
|
|
325
|
+
temp_directory = self._get_temp_directory()
|
|
326
|
+
|
|
327
|
+
if not hash_key:
|
|
328
|
+
hash_key = str(uuid4())
|
|
329
|
+
|
|
330
|
+
if hash_filename:
|
|
331
|
+
hash_key = sha1_hash(hash_key)
|
|
332
|
+
|
|
333
|
+
return "{0}/pyinfra-{1}".format(temp_directory, hash_key)
|
|
334
|
+
|
|
335
|
+
# Host facts
|
|
336
|
+
#
|
|
337
|
+
|
|
338
|
+
T = TypeVar("T")
|
|
339
|
+
|
|
340
|
+
@overload
|
|
341
|
+
def get_fact(self, name_or_cls: Type[FactBase[T]], *args, **kwargs) -> T: ...
|
|
342
|
+
|
|
343
|
+
@overload
|
|
344
|
+
def get_fact(self, name_or_cls: Type[ShortFactBase[T]], *args, **kwargs) -> T: ...
|
|
345
|
+
|
|
346
|
+
def get_fact(self, name_or_cls, *args, **kwargs):
|
|
271
347
|
"""
|
|
272
|
-
|
|
348
|
+
Get a fact for this host, reading from the cache if present.
|
|
273
349
|
"""
|
|
274
|
-
return
|
|
350
|
+
return get_host_fact(self.state, self, name_or_cls, args=args, kwargs=kwargs)
|
|
275
351
|
|
|
276
352
|
# Connector proxy
|
|
277
353
|
#
|
|
@@ -286,11 +362,11 @@ class Host:
|
|
|
286
362
|
"""
|
|
287
363
|
|
|
288
364
|
self._check_state()
|
|
289
|
-
if not self.
|
|
365
|
+
if not self.connected:
|
|
290
366
|
self.state.trigger_callbacks("host_before_connect", self)
|
|
291
367
|
|
|
292
368
|
try:
|
|
293
|
-
self.
|
|
369
|
+
self.connector.connect()
|
|
294
370
|
except ConnectError as e:
|
|
295
371
|
if show_errors:
|
|
296
372
|
log_message = "{0}{1}".format(
|
|
@@ -316,8 +392,7 @@ class Host:
|
|
|
316
392
|
|
|
317
393
|
logger.info(log_message)
|
|
318
394
|
self.state.trigger_callbacks("host_connect", self)
|
|
319
|
-
|
|
320
|
-
return self.connection
|
|
395
|
+
self.connected = True
|
|
321
396
|
|
|
322
397
|
def disconnect(self):
|
|
323
398
|
"""
|
|
@@ -325,50 +400,43 @@ class Host:
|
|
|
325
400
|
"""
|
|
326
401
|
self._check_state()
|
|
327
402
|
|
|
328
|
-
# Disconnect is an optional function for
|
|
329
|
-
disconnect_func = getattr(self.
|
|
403
|
+
# Disconnect is an optional function for connectors if needed
|
|
404
|
+
disconnect_func = getattr(self.connector, "disconnect", None)
|
|
330
405
|
if disconnect_func:
|
|
331
|
-
return disconnect_func(
|
|
406
|
+
return disconnect_func()
|
|
332
407
|
|
|
333
408
|
# TODO: consider whether this should be here!
|
|
334
409
|
remove_any_sudo_askpass_file(self)
|
|
335
410
|
|
|
336
411
|
self.state.trigger_callbacks("host_disconnect", self)
|
|
337
412
|
|
|
338
|
-
def run_shell_command(self, *args, **kwargs):
|
|
413
|
+
def run_shell_command(self, *args, **kwargs) -> tuple[bool, CommandOutput]:
|
|
339
414
|
"""
|
|
340
415
|
Low level method to execute a shell command on the host via it's configured connector.
|
|
341
416
|
"""
|
|
342
417
|
self._check_state()
|
|
343
|
-
return self.
|
|
418
|
+
return self.connector.run_shell_command(*args, **kwargs)
|
|
344
419
|
|
|
345
|
-
def put_file(self, *args, **kwargs):
|
|
420
|
+
def put_file(self, *args, **kwargs) -> bool:
|
|
346
421
|
"""
|
|
347
422
|
Low level method to upload a file to the host via it's configured connector.
|
|
348
423
|
"""
|
|
349
424
|
self._check_state()
|
|
350
|
-
return self.
|
|
425
|
+
return self.connector.put_file(*args, **kwargs)
|
|
351
426
|
|
|
352
|
-
def get_file(self, *args, **kwargs):
|
|
427
|
+
def get_file(self, *args, **kwargs) -> bool:
|
|
353
428
|
"""
|
|
354
429
|
Low level method to download a file from the host via it's configured connector.
|
|
355
430
|
"""
|
|
356
431
|
self._check_state()
|
|
357
|
-
return self.
|
|
358
|
-
|
|
359
|
-
# Rsync - optional executor specific ability
|
|
432
|
+
return self.connector.get_file(*args, **kwargs)
|
|
360
433
|
|
|
361
|
-
|
|
362
|
-
check_can_rsync_func = getattr(self.executor, "check_can_rsync", None)
|
|
363
|
-
if check_can_rsync_func:
|
|
364
|
-
return check_can_rsync_func(self)
|
|
434
|
+
# Rsync - optional connector specific ability
|
|
365
435
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
),
|
|
370
|
-
)
|
|
436
|
+
def check_can_rsync(self) -> None:
|
|
437
|
+
self._check_state()
|
|
438
|
+
return self.connector.check_can_rsync()
|
|
371
439
|
|
|
372
|
-
def rsync(self, *args, **kwargs):
|
|
440
|
+
def rsync(self, *args, **kwargs) -> bool:
|
|
373
441
|
self._check_state()
|
|
374
|
-
return self.
|
|
442
|
+
return self.connector.rsync(*args, **kwargs)
|
pyinfra/api/inventory.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections import defaultdict
|
|
2
|
-
from typing import TYPE_CHECKING, Any, Iterator
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Iterator
|
|
3
5
|
|
|
4
6
|
from .connectors import get_all_connectors, get_execution_connectors
|
|
5
7
|
from .exceptions import NoConnectorError, NoGroupError, NoHostError
|
|
@@ -9,7 +11,7 @@ if TYPE_CHECKING:
|
|
|
9
11
|
from pyinfra.api.state import State
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
def extract_name_data(names:
|
|
14
|
+
def extract_name_data(names: list[Any]):
|
|
13
15
|
for name in names:
|
|
14
16
|
data = {}
|
|
15
17
|
|
|
@@ -35,11 +37,15 @@ class Inventory:
|
|
|
35
37
|
|
|
36
38
|
state: "State"
|
|
37
39
|
|
|
40
|
+
@staticmethod
|
|
41
|
+
def empty():
|
|
42
|
+
return Inventory(([], {}))
|
|
43
|
+
|
|
38
44
|
def __init__(self, names_data, override_data=None, **groups):
|
|
39
45
|
# Setup basics
|
|
40
46
|
self.groups = defaultdict(list) # lists of Host objects
|
|
41
|
-
self.host_data = defaultdict(dict) # dict of name -> data
|
|
42
|
-
self.group_data = defaultdict(dict) # dict of name -> data
|
|
47
|
+
self.host_data: dict[str, dict] = defaultdict(dict) # dict of name -> data
|
|
48
|
+
self.group_data: dict[str, dict] = defaultdict(dict) # dict of name -> data
|
|
43
49
|
self.override_data = override_data or {}
|
|
44
50
|
|
|
45
51
|
names, data = names_data
|
|
@@ -48,14 +54,14 @@ class Inventory:
|
|
|
48
54
|
self.data = data
|
|
49
55
|
|
|
50
56
|
# Create the actual host instances and groups
|
|
51
|
-
self.
|
|
57
|
+
self.make_hosts_and_groups(names, groups)
|
|
52
58
|
|
|
53
|
-
def make_hosts_and_groups(self, names, groups):
|
|
59
|
+
def make_hosts_and_groups(self, names, groups) -> None:
|
|
54
60
|
all_connectors = get_all_connectors()
|
|
55
61
|
execution_connectors = get_execution_connectors()
|
|
56
62
|
|
|
57
63
|
# Map name -> data
|
|
58
|
-
name_to_data = defaultdict(dict)
|
|
64
|
+
name_to_data: dict[str, dict] = defaultdict(dict)
|
|
59
65
|
# Map name -> group names
|
|
60
66
|
name_to_group_names = defaultdict(list)
|
|
61
67
|
|
|
@@ -73,14 +79,14 @@ class Inventory:
|
|
|
73
79
|
for name, data in extract_name_data(names):
|
|
74
80
|
name_to_data[name].update(data)
|
|
75
81
|
|
|
76
|
-
# Now, use the above to fill self.host_data and populate
|
|
77
|
-
|
|
82
|
+
# Now, use the above to fill self.host_data and populate names_connectors
|
|
83
|
+
names_connectors = []
|
|
78
84
|
|
|
79
85
|
for name, _ in extract_name_data(names):
|
|
80
86
|
host_data = name_to_data[name]
|
|
81
87
|
|
|
82
88
|
# Default to executing commands with the ssh connector
|
|
83
|
-
|
|
89
|
+
connector_cls = execution_connectors["ssh"]
|
|
84
90
|
|
|
85
91
|
if name[0] == "@":
|
|
86
92
|
connector_name = name[1:]
|
|
@@ -96,7 +102,7 @@ class Inventory:
|
|
|
96
102
|
|
|
97
103
|
# Execution connector? Simple, just set it for their host
|
|
98
104
|
if connector_name in execution_connectors:
|
|
99
|
-
|
|
105
|
+
connector_cls = execution_connectors[connector_name]
|
|
100
106
|
|
|
101
107
|
names_data = all_connectors[connector_name].make_names_data(arg_string)
|
|
102
108
|
connector_inventory_name = name
|
|
@@ -111,7 +117,7 @@ class Inventory:
|
|
|
111
117
|
|
|
112
118
|
# Assign the name/data/groups from the connector
|
|
113
119
|
self.host_data[sub_name] = sub_data
|
|
114
|
-
|
|
120
|
+
names_connectors.append((sub_name, connector_cls))
|
|
115
121
|
name_to_group_names[sub_name].extend(sub_groups)
|
|
116
122
|
|
|
117
123
|
# If we have a connector inventory name, copy any groups attached
|
|
@@ -124,10 +130,10 @@ class Inventory:
|
|
|
124
130
|
# Now we can actually make Host instances
|
|
125
131
|
hosts: dict[str, "Host"] = {}
|
|
126
132
|
|
|
127
|
-
for name,
|
|
133
|
+
for name, connector_cls in names_connectors:
|
|
128
134
|
host_groups = name_to_group_names[name]
|
|
129
135
|
|
|
130
|
-
host = Host(name, inventory=self, groups=host_groups,
|
|
136
|
+
host = Host(name, inventory=self, groups=host_groups, connector_cls=connector_cls)
|
|
131
137
|
hosts[name] = host
|
|
132
138
|
|
|
133
139
|
# And push into any groups
|
|
@@ -135,7 +141,7 @@ class Inventory:
|
|
|
135
141
|
if host not in self.groups[group_name]:
|
|
136
142
|
self.groups[group_name].append(host)
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
self.hosts = hosts
|
|
139
145
|
|
|
140
146
|
def __len__(self) -> int:
|
|
141
147
|
"""
|
|
@@ -241,13 +247,3 @@ class Inventory:
|
|
|
241
247
|
data.update(self.get_group_data(group))
|
|
242
248
|
|
|
243
249
|
return data
|
|
244
|
-
|
|
245
|
-
def get_deploy_data(self):
|
|
246
|
-
"""
|
|
247
|
-
Gets any default data attached to the current deploy, if any.
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
if self.state and self.state.deploy_data:
|
|
251
|
-
return self.state.deploy_data
|
|
252
|
-
|
|
253
|
-
return {}
|