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/state.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import IntEnum
|
|
2
6
|
from graphlib import CycleError, TopologicalSorter
|
|
3
7
|
from multiprocessing import cpu_count
|
|
4
|
-
from typing import TYPE_CHECKING,
|
|
5
|
-
from uuid import uuid4
|
|
8
|
+
from typing import TYPE_CHECKING, Callable, Iterator, Optional
|
|
6
9
|
|
|
7
10
|
from gevent.pool import Pool
|
|
8
11
|
from paramiko import PKey
|
|
@@ -10,12 +13,14 @@ from paramiko import PKey
|
|
|
10
13
|
from pyinfra import logger
|
|
11
14
|
|
|
12
15
|
from .config import Config
|
|
13
|
-
from .exceptions import
|
|
14
|
-
from .util import sha1_hash
|
|
16
|
+
from .exceptions import PyinfraError
|
|
15
17
|
|
|
16
18
|
if TYPE_CHECKING:
|
|
19
|
+
from pyinfra.api.arguments import AllArguments
|
|
20
|
+
from pyinfra.api.command import PyinfraCommand
|
|
17
21
|
from pyinfra.api.host import Host
|
|
18
22
|
from pyinfra.api.inventory import Inventory
|
|
23
|
+
from pyinfra.api.operation import OperationMeta
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
# Work out the max parallel we can achieve with the open files limit of the user/process,
|
|
@@ -77,6 +82,58 @@ class BaseStateCallback:
|
|
|
77
82
|
pass
|
|
78
83
|
|
|
79
84
|
|
|
85
|
+
class StateStage(IntEnum):
|
|
86
|
+
# Setup - collect inventory & data
|
|
87
|
+
Setup = 1
|
|
88
|
+
# Connect - connect to the inventory
|
|
89
|
+
Connect = 2
|
|
90
|
+
# Prepare - detect operation changes
|
|
91
|
+
Prepare = 3
|
|
92
|
+
# Execute - execute operations
|
|
93
|
+
Execute = 4
|
|
94
|
+
# Disconnect - disconnect from the inventory
|
|
95
|
+
Disconnect = 5
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class StateOperationMeta:
|
|
99
|
+
names: set[str]
|
|
100
|
+
args: list[str]
|
|
101
|
+
op_order: tuple[int, ...]
|
|
102
|
+
global_arguments: "AllArguments"
|
|
103
|
+
|
|
104
|
+
def __init__(self, op_order: tuple[int, ...]):
|
|
105
|
+
self.op_order = op_order
|
|
106
|
+
self.names = set()
|
|
107
|
+
self.args = []
|
|
108
|
+
self.global_arguments = {} # type: ignore
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class StateOperationHostData:
|
|
113
|
+
command_generator: Callable[[], Iterator["PyinfraCommand"]]
|
|
114
|
+
global_arguments: "AllArguments"
|
|
115
|
+
operation_meta: "OperationMeta"
|
|
116
|
+
parent_op_hash: Optional[str] = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class StateHostMeta:
|
|
120
|
+
ops = 0
|
|
121
|
+
ops_change = 0
|
|
122
|
+
ops_no_change = 0
|
|
123
|
+
op_hashes: set[str]
|
|
124
|
+
|
|
125
|
+
def __init__(self):
|
|
126
|
+
self.op_hashes = set()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class StateHostResults:
|
|
130
|
+
ops = 0
|
|
131
|
+
success_ops = 0
|
|
132
|
+
error_ops = 0
|
|
133
|
+
ignored_error_ops = 0
|
|
134
|
+
partial_ops = 0
|
|
135
|
+
|
|
136
|
+
|
|
80
137
|
class State:
|
|
81
138
|
"""
|
|
82
139
|
Manages state for a pyinfra deploy.
|
|
@@ -93,9 +150,18 @@ class State:
|
|
|
93
150
|
# Main gevent pool
|
|
94
151
|
pool: "Pool"
|
|
95
152
|
|
|
153
|
+
# Current stage this state is in
|
|
154
|
+
current_stage: StateStage = StateStage.Setup
|
|
155
|
+
# Warning counters by stage
|
|
156
|
+
stage_warnings: dict[StateStage, int] = defaultdict(int)
|
|
157
|
+
|
|
96
158
|
# Whether we are executing operations (ie hosts are all ready)
|
|
97
159
|
is_executing: bool = False
|
|
98
160
|
|
|
161
|
+
# Whether we should check for operation changes as part of the operation ordering phase, this
|
|
162
|
+
# allows us to guesstimate which ops will result in changes on which hosts.
|
|
163
|
+
check_for_changes: bool = True
|
|
164
|
+
|
|
99
165
|
print_noop_info: bool = False # print "[host] noop: reason for noop"
|
|
100
166
|
print_fact_info: bool = False # print "loaded fact X"
|
|
101
167
|
print_input: bool = False
|
|
@@ -108,20 +174,33 @@ class State:
|
|
|
108
174
|
current_deploy_filename: Optional[str] = None
|
|
109
175
|
current_exec_filename: Optional[str] = None
|
|
110
176
|
current_op_file_number: int = 0
|
|
177
|
+
should_raise_failed_hosts: Optional[Callable[["State"], bool]] = None
|
|
111
178
|
|
|
112
179
|
def __init__(
|
|
113
|
-
self,
|
|
180
|
+
self,
|
|
181
|
+
inventory: Optional["Inventory"] = None,
|
|
182
|
+
config: Optional["Config"] = None,
|
|
183
|
+
check_for_changes: bool = True,
|
|
184
|
+
**kwargs,
|
|
114
185
|
):
|
|
115
|
-
"""
|
|
186
|
+
"""
|
|
187
|
+
Initializes the state, the main Pyinfra
|
|
116
188
|
|
|
117
189
|
Args:
|
|
118
190
|
inventory (Optional[Inventory], optional): The inventory. Defaults to None.
|
|
119
191
|
config (Optional[Config], optional): The config object. Defaults to None.
|
|
120
192
|
"""
|
|
193
|
+
self.check_for_changes = check_for_changes
|
|
194
|
+
|
|
121
195
|
if inventory:
|
|
122
196
|
self.init(inventory, config, **kwargs)
|
|
123
197
|
|
|
124
|
-
def init(
|
|
198
|
+
def init(
|
|
199
|
+
self,
|
|
200
|
+
inventory: "Inventory",
|
|
201
|
+
config: Optional["Config"],
|
|
202
|
+
initial_limit=None,
|
|
203
|
+
):
|
|
125
204
|
# Config validation
|
|
126
205
|
#
|
|
127
206
|
|
|
@@ -150,79 +229,63 @@ class State:
|
|
|
150
229
|
# Actually initialise the state object
|
|
151
230
|
#
|
|
152
231
|
|
|
153
|
-
self.callback_handlers:
|
|
232
|
+
self.callback_handlers: list[BaseStateCallback] = []
|
|
154
233
|
|
|
155
234
|
# Setup greenlet pools
|
|
156
235
|
self.pool = Pool(config.PARALLEL)
|
|
157
236
|
self.fact_pool = Pool(config.PARALLEL)
|
|
158
237
|
|
|
159
238
|
# Private keys
|
|
160
|
-
self.private_keys:
|
|
239
|
+
self.private_keys: dict[str, PKey] = {}
|
|
161
240
|
|
|
162
241
|
# Assign inventory/config
|
|
163
242
|
self.inventory = inventory
|
|
164
243
|
self.config = config
|
|
165
244
|
|
|
166
245
|
# Hosts we've activated at any time
|
|
167
|
-
self.activated_hosts:
|
|
246
|
+
self.activated_hosts: set["Host"] = set()
|
|
168
247
|
# Active hosts that *haven't* failed yet
|
|
169
|
-
self.active_hosts:
|
|
248
|
+
self.active_hosts: set["Host"] = set()
|
|
170
249
|
# Hosts that have failed
|
|
171
|
-
self.failed_hosts:
|
|
250
|
+
self.failed_hosts: set["Host"] = set()
|
|
172
251
|
|
|
173
252
|
# Limit hosts changes dynamically to limit operations to a subset of hosts
|
|
174
|
-
self.limit_hosts:
|
|
253
|
+
self.limit_hosts: list["Host"] = initial_limit
|
|
175
254
|
|
|
176
255
|
# Op basics
|
|
177
|
-
self.op_meta:
|
|
178
|
-
self.ops_run: Set[str] = set() # list of ops which have been started/run
|
|
256
|
+
self.op_meta: dict[str, StateOperationMeta] = {} # maps operation hash -> names/etc
|
|
179
257
|
|
|
180
258
|
# Op dict for each host
|
|
181
|
-
self.ops:
|
|
182
|
-
|
|
183
|
-
# Facts dict for each host
|
|
184
|
-
self.facts: Dict["Host", Any] = {host: {} for host in inventory}
|
|
259
|
+
self.ops: dict["Host", dict[str, StateOperationHostData]] = {host: {} for host in inventory}
|
|
185
260
|
|
|
186
261
|
# Meta dict for each host
|
|
187
|
-
self.meta = {
|
|
188
|
-
host: {
|
|
189
|
-
"ops": 0, # one function call in a deploy file
|
|
190
|
-
"ops_change": 0,
|
|
191
|
-
"ops_no_change": 0,
|
|
192
|
-
"commands": 0, # actual # of commands to run
|
|
193
|
-
"op_hashes": set(),
|
|
194
|
-
}
|
|
195
|
-
for host in inventory
|
|
196
|
-
}
|
|
262
|
+
self.meta: dict["Host", StateHostMeta] = {host: StateHostMeta() for host in inventory}
|
|
197
263
|
|
|
198
264
|
# Results dict for each host
|
|
199
|
-
self.results = {
|
|
200
|
-
host:
|
|
201
|
-
"ops": 0, # success_ops + failed ops w/ignore_errors
|
|
202
|
-
"success_ops": 0,
|
|
203
|
-
"error_ops": 0,
|
|
204
|
-
"ignored_error_ops": 0,
|
|
205
|
-
"partial_ops": 0, # operations that had an error, but did do something
|
|
206
|
-
"commands": 0,
|
|
207
|
-
}
|
|
208
|
-
for host in inventory
|
|
265
|
+
self.results: dict["Host", StateHostResults] = {
|
|
266
|
+
host: StateHostResults() for host in inventory
|
|
209
267
|
}
|
|
210
268
|
|
|
211
269
|
# Assign state back references to inventory & config
|
|
212
270
|
inventory.state = config.state = self
|
|
213
271
|
for host in inventory:
|
|
214
|
-
host.
|
|
272
|
+
host.init(self)
|
|
215
273
|
|
|
216
274
|
self.initialised = True
|
|
217
275
|
|
|
218
|
-
def
|
|
219
|
-
|
|
220
|
-
"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
def set_stage(self, stage: StateStage) -> None:
|
|
277
|
+
if stage < self.current_stage:
|
|
278
|
+
raise Exception("State stage cannot go backwards!")
|
|
279
|
+
self.current_stage = stage
|
|
280
|
+
|
|
281
|
+
def increment_warning_counter(self) -> None:
|
|
282
|
+
self.stage_warnings[self.current_stage] += 1
|
|
283
|
+
|
|
284
|
+
def get_warning_counter(self) -> int:
|
|
285
|
+
return self.stage_warnings[self.current_stage]
|
|
286
|
+
|
|
287
|
+
def should_check_for_changes(self):
|
|
288
|
+
return self.check_for_changes
|
|
226
289
|
|
|
227
290
|
def add_callback_handler(self, handler):
|
|
228
291
|
if not isinstance(handler, BaseStateCallback):
|
|
@@ -236,18 +299,8 @@ class State:
|
|
|
236
299
|
func = getattr(handler, method_name)
|
|
237
300
|
func(self, *args, **kwargs)
|
|
238
301
|
|
|
239
|
-
@contextmanager
|
|
240
|
-
def preserve_loop_order(self, items):
|
|
241
|
-
logger.warning(
|
|
242
|
-
(
|
|
243
|
-
"Using `state.preserve_loop_order` is not longer required for operations to be "
|
|
244
|
-
"executed in correct loop order and can be safely removed."
|
|
245
|
-
),
|
|
246
|
-
)
|
|
247
|
-
yield lambda: items
|
|
248
|
-
|
|
249
302
|
def get_op_order(self):
|
|
250
|
-
ts = TopologicalSorter()
|
|
303
|
+
ts: TopologicalSorter = TopologicalSorter()
|
|
251
304
|
|
|
252
305
|
for host in self.inventory:
|
|
253
306
|
for i, op_hash in enumerate(host.op_hash_order):
|
|
@@ -275,20 +328,35 @@ class State:
|
|
|
275
328
|
# dependency order we order them by line numbers.
|
|
276
329
|
node_group = sorted(
|
|
277
330
|
ts.get_ready(),
|
|
278
|
-
key=lambda op_hash: self.op_meta[op_hash]
|
|
331
|
+
key=lambda op_hash: self.op_meta[op_hash].op_order,
|
|
279
332
|
)
|
|
280
333
|
ts.done(*node_group)
|
|
281
334
|
final_op_order.extend(node_group)
|
|
282
335
|
|
|
283
336
|
return final_op_order
|
|
284
337
|
|
|
285
|
-
def get_op_meta(self, op_hash: str):
|
|
338
|
+
def get_op_meta(self, op_hash: str) -> StateOperationMeta:
|
|
286
339
|
return self.op_meta[op_hash]
|
|
287
340
|
|
|
288
|
-
def
|
|
341
|
+
def get_meta_for_host(self, host: "Host") -> StateHostMeta:
|
|
342
|
+
return self.meta[host]
|
|
343
|
+
|
|
344
|
+
def get_results_for_host(self, host: "Host") -> StateHostResults:
|
|
345
|
+
return self.results[host]
|
|
346
|
+
|
|
347
|
+
def get_op_data_for_host(
|
|
348
|
+
self,
|
|
349
|
+
host: "Host",
|
|
350
|
+
op_hash: str,
|
|
351
|
+
) -> StateOperationHostData:
|
|
289
352
|
return self.ops[host][op_hash]
|
|
290
353
|
|
|
291
|
-
def
|
|
354
|
+
def set_op_data_for_host(
|
|
355
|
+
self,
|
|
356
|
+
host: "Host",
|
|
357
|
+
op_hash: str,
|
|
358
|
+
op_data: StateOperationHostData,
|
|
359
|
+
):
|
|
292
360
|
self.ops[host][op_hash] = op_data
|
|
293
361
|
|
|
294
362
|
def activate_host(self, host: "Host"):
|
|
@@ -330,13 +398,16 @@ class State:
|
|
|
330
398
|
|
|
331
399
|
# No hosts left!
|
|
332
400
|
if not active_hosts:
|
|
333
|
-
raise
|
|
401
|
+
raise PyinfraError("No hosts remaining!")
|
|
334
402
|
|
|
335
403
|
if self.config.FAIL_PERCENT is not None:
|
|
336
404
|
percent_failed = (1 - len(active_hosts) / activated_count) * 100
|
|
337
405
|
|
|
338
406
|
if percent_failed > self.config.FAIL_PERCENT:
|
|
339
|
-
|
|
407
|
+
if self.should_raise_failed_hosts and self.should_raise_failed_hosts(self) is False:
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
raise PyinfraError(
|
|
340
411
|
"Over {0}% of hosts failed ({1}%)".format(
|
|
341
412
|
self.config.FAIL_PERCENT,
|
|
342
413
|
int(round(percent_failed)),
|
|
@@ -353,16 +424,3 @@ class State:
|
|
|
353
424
|
if not isinstance(limit_hosts, list):
|
|
354
425
|
return True
|
|
355
426
|
return host in limit_hosts
|
|
356
|
-
|
|
357
|
-
def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
|
|
358
|
-
"""
|
|
359
|
-
Generate a temporary filename for this deploy.
|
|
360
|
-
"""
|
|
361
|
-
|
|
362
|
-
if not hash_key:
|
|
363
|
-
hash_key = str(uuid4())
|
|
364
|
-
|
|
365
|
-
if hash_filename:
|
|
366
|
-
hash_key = sha1_hash(hash_key)
|
|
367
|
-
|
|
368
|
-
return "{0}/pyinfra-{1}".format(self.config.TEMP_DIR, hash_key)
|
pyinfra/api/util.py
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from functools import wraps
|
|
2
4
|
from hashlib import sha1
|
|
3
|
-
from inspect import getframeinfo,
|
|
5
|
+
from inspect import getframeinfo, stack
|
|
4
6
|
from io import BytesIO, StringIO
|
|
5
7
|
from os import getcwd, path, stat
|
|
6
8
|
from socket import error as socket_error, timeout as timeout_error
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional,
|
|
9
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
|
|
8
10
|
|
|
9
11
|
import click
|
|
10
12
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
11
13
|
from paramiko import SSHException
|
|
14
|
+
from typeguard import TypeCheckError, check_type
|
|
12
15
|
|
|
13
16
|
import pyinfra
|
|
14
17
|
from pyinfra import logger
|
|
15
18
|
|
|
16
19
|
if TYPE_CHECKING:
|
|
17
20
|
from pyinfra.api.host import Host
|
|
18
|
-
from pyinfra.api.state import State
|
|
21
|
+
from pyinfra.api.state import State, StateOperationMeta
|
|
22
|
+
from pyinfra.connectors.util import CommandOutput
|
|
19
23
|
|
|
20
24
|
# 64kb chunks
|
|
21
25
|
BLOCKSIZE = 65536
|
|
@@ -24,7 +28,7 @@ BLOCKSIZE = 65536
|
|
|
24
28
|
TEMPLATES: Dict[Any, Any] = {}
|
|
25
29
|
FILE_SHAS: Dict[Any, Any] = {}
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
PYINFRA_INSTALL_DIR = path.normpath(path.join(path.dirname(__file__), ".."))
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def get_file_path(state: "State", filename: str):
|
|
@@ -40,28 +44,6 @@ def get_file_path(state: "State", filename: str):
|
|
|
40
44
|
return path.join(relative_to, filename)
|
|
41
45
|
|
|
42
46
|
|
|
43
|
-
def get_args_kwargs_spec(func: Callable[..., Any]) -> Tuple[List, Dict]:
|
|
44
|
-
args: List[Any] = []
|
|
45
|
-
kwargs: Dict[Any, Any] = {}
|
|
46
|
-
|
|
47
|
-
argspec = getfullargspec(func)
|
|
48
|
-
if not argspec.args:
|
|
49
|
-
return args, kwargs
|
|
50
|
-
|
|
51
|
-
if argspec.defaults:
|
|
52
|
-
kwargs = dict(
|
|
53
|
-
zip(
|
|
54
|
-
argspec.args[-len(argspec.defaults) :],
|
|
55
|
-
argspec.defaults,
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
args = argspec.args[: -len(argspec.defaults)]
|
|
59
|
-
else:
|
|
60
|
-
args = argspec.args
|
|
61
|
-
|
|
62
|
-
return args, kwargs
|
|
63
|
-
|
|
64
|
-
|
|
65
47
|
def get_kwargs_str(kwargs: Dict[Any, Any]):
|
|
66
48
|
if not kwargs:
|
|
67
49
|
return ""
|
|
@@ -85,14 +67,14 @@ def memoize(func: Callable[..., Any]):
|
|
|
85
67
|
@wraps(func)
|
|
86
68
|
def wrapper(*args, **kwargs):
|
|
87
69
|
key = "{0}{1}".format(args, kwargs)
|
|
88
|
-
if key in wrapper.cache:
|
|
89
|
-
return wrapper.cache[key]
|
|
70
|
+
if key in wrapper.cache: # type: ignore[attr-defined]
|
|
71
|
+
return wrapper.cache[key] # type: ignore[attr-defined]
|
|
90
72
|
|
|
91
73
|
value = func(*args, **kwargs)
|
|
92
|
-
wrapper.cache[key] = value
|
|
74
|
+
wrapper.cache[key] = value # type: ignore[attr-defined]
|
|
93
75
|
return value
|
|
94
76
|
|
|
95
|
-
wrapper.cache = {} # type: ignore
|
|
77
|
+
wrapper.cache = {} # type: ignore[attr-defined]
|
|
96
78
|
return wrapper
|
|
97
79
|
|
|
98
80
|
|
|
@@ -127,16 +109,16 @@ def get_caller_frameinfo(frame_offset: int = 0):
|
|
|
127
109
|
|
|
128
110
|
|
|
129
111
|
def get_operation_order_from_stack(state: "State"):
|
|
112
|
+
|
|
130
113
|
stack_items = list(reversed(stack()))
|
|
131
114
|
|
|
115
|
+
i = 0
|
|
132
116
|
# Find the *first* occurrence of our deploy file in the reversed stack
|
|
133
117
|
if state.current_deploy_filename:
|
|
134
118
|
for i, stack_item in enumerate(stack_items):
|
|
135
119
|
frame = getframeinfo(stack_item[0])
|
|
136
120
|
if frame.filename == state.current_deploy_filename:
|
|
137
121
|
break
|
|
138
|
-
else:
|
|
139
|
-
i = 0
|
|
140
122
|
|
|
141
123
|
# Now generate a list of line numbers *following that file*
|
|
142
124
|
line_numbers = []
|
|
@@ -147,7 +129,7 @@ def get_operation_order_from_stack(state: "State"):
|
|
|
147
129
|
for stack_item in stack_items[i:]:
|
|
148
130
|
frame = getframeinfo(stack_item[0])
|
|
149
131
|
|
|
150
|
-
if frame.filename.startswith(
|
|
132
|
+
if frame.filename.startswith(PYINFRA_INSTALL_DIR):
|
|
151
133
|
continue
|
|
152
134
|
|
|
153
135
|
line_numbers.append(frame.lineno)
|
|
@@ -157,7 +139,7 @@ def get_operation_order_from_stack(state: "State"):
|
|
|
157
139
|
return line_numbers
|
|
158
140
|
|
|
159
141
|
|
|
160
|
-
def get_template(filename_or_io: str):
|
|
142
|
+
def get_template(filename_or_io: str | IO):
|
|
161
143
|
"""
|
|
162
144
|
Gets a jinja2 ``Template`` object for the input filename or string, with caching
|
|
163
145
|
based on the filename of the template, or the SHA1 of the input string.
|
|
@@ -184,7 +166,7 @@ def get_template(filename_or_io: str):
|
|
|
184
166
|
return template
|
|
185
167
|
|
|
186
168
|
|
|
187
|
-
def sha1_hash(string: str):
|
|
169
|
+
def sha1_hash(string: str) -> str:
|
|
188
170
|
"""
|
|
189
171
|
Return the SHA1 of the input string.
|
|
190
172
|
"""
|
|
@@ -194,38 +176,30 @@ def sha1_hash(string: str):
|
|
|
194
176
|
return hasher.hexdigest()
|
|
195
177
|
|
|
196
178
|
|
|
197
|
-
def format_exception(e):
|
|
198
|
-
return "{
|
|
179
|
+
def format_exception(e: Exception) -> str:
|
|
180
|
+
return f"{e.__class__.__name__}{e.args}"
|
|
199
181
|
|
|
200
182
|
|
|
201
|
-
def print_host_combined_output(host: "Host",
|
|
202
|
-
for
|
|
203
|
-
if
|
|
204
|
-
logger.error(
|
|
205
|
-
"{0}{1}".format(
|
|
206
|
-
host.print_prefix,
|
|
207
|
-
click.style(line, "red"),
|
|
208
|
-
),
|
|
209
|
-
)
|
|
183
|
+
def print_host_combined_output(host: "Host", output: "CommandOutput") -> None:
|
|
184
|
+
for line in output:
|
|
185
|
+
if line.buffer_name == "stderr":
|
|
186
|
+
logger.error(f"{host.print_prefix}{click.style(line.line, 'red')}")
|
|
210
187
|
else:
|
|
211
|
-
logger.error(
|
|
212
|
-
"{0}{1}".format(
|
|
213
|
-
host.print_prefix,
|
|
214
|
-
line,
|
|
215
|
-
),
|
|
216
|
-
)
|
|
188
|
+
logger.error(f"{host.print_prefix}{line.line}")
|
|
217
189
|
|
|
218
190
|
|
|
219
|
-
def log_operation_start(
|
|
191
|
+
def log_operation_start(
|
|
192
|
+
op_meta: "StateOperationMeta", op_types: Optional[List] = None, prefix: str = "--> "
|
|
193
|
+
) -> None:
|
|
220
194
|
op_types = op_types or []
|
|
221
|
-
if op_meta["
|
|
195
|
+
if op_meta.global_arguments["_serial"]:
|
|
222
196
|
op_types.append("serial")
|
|
223
|
-
if op_meta["
|
|
197
|
+
if op_meta.global_arguments["_run_once"]:
|
|
224
198
|
op_types.append("run once")
|
|
225
199
|
|
|
226
200
|
args = ""
|
|
227
|
-
if op_meta
|
|
228
|
-
args = "({0})".format(", ".join(str(arg) for arg in op_meta
|
|
201
|
+
if op_meta.args:
|
|
202
|
+
args = "({0})".format(", ".join(str(arg) for arg in op_meta.args))
|
|
229
203
|
|
|
230
204
|
logger.info(
|
|
231
205
|
"{0} {1} {2}".format(
|
|
@@ -236,7 +210,7 @@ def log_operation_start(op_meta: Dict, op_types: Optional[List] = None, prefix:
|
|
|
236
210
|
),
|
|
237
211
|
"blue",
|
|
238
212
|
),
|
|
239
|
-
click.style(", ".join(op_meta
|
|
213
|
+
click.style(", ".join(op_meta.names), bold=True),
|
|
240
214
|
args,
|
|
241
215
|
),
|
|
242
216
|
)
|
|
@@ -244,7 +218,7 @@ def log_operation_start(op_meta: Dict, op_types: Optional[List] = None, prefix:
|
|
|
244
218
|
|
|
245
219
|
def log_error_or_warning(
|
|
246
220
|
host: "Host", ignore_errors: bool, description: str = "", continue_on_error: bool = False
|
|
247
|
-
):
|
|
221
|
+
) -> None:
|
|
248
222
|
log_func = logger.error
|
|
249
223
|
log_color = "red"
|
|
250
224
|
log_text = "Error: " if description else "Error"
|
|
@@ -267,7 +241,7 @@ def log_error_or_warning(
|
|
|
267
241
|
)
|
|
268
242
|
|
|
269
243
|
|
|
270
|
-
def log_host_command_error(host: "Host", e, timeout: int = 0):
|
|
244
|
+
def log_host_command_error(host: "Host", e: Exception, timeout: int = 0) -> None:
|
|
271
245
|
if isinstance(e, timeout_error):
|
|
272
246
|
logger.error(
|
|
273
247
|
"{0}{1}".format(
|
|
@@ -327,19 +301,27 @@ def make_hash(obj):
|
|
|
327
301
|
if isinstance(obj, int)
|
|
328
302
|
# Constants - the values can change between hosts but we should still
|
|
329
303
|
# group them under the same operation hash.
|
|
330
|
-
else
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
304
|
+
else (
|
|
305
|
+
"_PYINFRA_CONSTANT"
|
|
306
|
+
if obj in (True, False, None)
|
|
307
|
+
# Plain strings
|
|
308
|
+
else (
|
|
309
|
+
obj
|
|
310
|
+
if isinstance(obj, str)
|
|
311
|
+
# Objects with __name__s
|
|
312
|
+
else (
|
|
313
|
+
obj.__name__
|
|
314
|
+
if hasattr(obj, "__name__")
|
|
315
|
+
# Objects with names
|
|
316
|
+
else (
|
|
317
|
+
obj.name
|
|
318
|
+
if hasattr(obj, "name")
|
|
319
|
+
# Repr anything else
|
|
320
|
+
else repr(obj)
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
)
|
|
343
325
|
)
|
|
344
326
|
|
|
345
327
|
return sha1_hash(hash_string)
|
|
@@ -351,7 +333,11 @@ class get_file_io:
|
|
|
351
333
|
will open and close filenames, and leave IO objects alone.
|
|
352
334
|
"""
|
|
353
335
|
|
|
354
|
-
|
|
336
|
+
filename_or_io: Union[str, IO[Any]]
|
|
337
|
+
mode: str
|
|
338
|
+
|
|
339
|
+
_close: bool = False
|
|
340
|
+
_file_io: IO[Any]
|
|
355
341
|
|
|
356
342
|
def __init__(self, filename_or_io, mode="rb"):
|
|
357
343
|
if not (
|
|
@@ -378,25 +364,20 @@ class get_file_io:
|
|
|
378
364
|
self.mode = mode
|
|
379
365
|
|
|
380
366
|
def __enter__(self):
|
|
381
|
-
|
|
382
|
-
if hasattr(self.filename_or_io, "read"):
|
|
383
|
-
file_io = self.filename_or_io
|
|
384
|
-
|
|
385
|
-
# Otherwise, assume a filename and open it up
|
|
386
|
-
else:
|
|
367
|
+
if isinstance(self.filename_or_io, str):
|
|
387
368
|
file_io = open(self.filename_or_io, self.mode)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
369
|
+
self._file_io = file_io
|
|
370
|
+
self._close = True
|
|
371
|
+
else:
|
|
372
|
+
file_io = self.filename_or_io
|
|
392
373
|
|
|
393
374
|
# Ensure we're at the start of the file
|
|
394
375
|
file_io.seek(0)
|
|
395
376
|
return file_io
|
|
396
377
|
|
|
397
378
|
def __exit__(self, type, value, traceback):
|
|
398
|
-
if self.
|
|
399
|
-
self.
|
|
379
|
+
if self._close:
|
|
380
|
+
self._file_io.close()
|
|
400
381
|
|
|
401
382
|
@property
|
|
402
383
|
def cache_key(self):
|
|
@@ -443,3 +424,15 @@ def get_path_permissions_mode(pathname: str):
|
|
|
443
424
|
|
|
444
425
|
mode_octal = oct(stat(pathname).st_mode)
|
|
445
426
|
return mode_octal[-3:]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def raise_if_bad_type(
|
|
430
|
+
value: Any,
|
|
431
|
+
type_: Type,
|
|
432
|
+
exception: type[Exception],
|
|
433
|
+
message_prefix: str,
|
|
434
|
+
):
|
|
435
|
+
try:
|
|
436
|
+
check_type(value, type_)
|
|
437
|
+
except TypeCheckError as e:
|
|
438
|
+
raise exception(f"{message_prefix}: {e}")
|