pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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 +18 -3
- pyinfra/api/arguments.py +406 -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 +67 -18
- pyinfra/api/facts.py +253 -202
- pyinfra/api/host.py +413 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +291 -368
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +381 -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 +670 -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 +410 -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 +630 -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 +746 -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 +98 -0
- pyinfra/operations/apt.py +488 -0
- pyinfra/operations/brew.py +231 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +191 -0
- pyinfra/operations/dnf.py +210 -0
- pyinfra/operations/docker.py +446 -0
- pyinfra/operations/files.py +1939 -0
- pyinfra/operations/flatpak.py +94 -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 +47 -0
- pyinfra/operations/git.py +419 -0
- pyinfra/operations/iptables.py +311 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +68 -0
- pyinfra/operations/mysql.py +609 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pacman.py +81 -0
- pyinfra/operations/pip.py +205 -0
- pyinfra/operations/pipx.py +102 -0
- pyinfra/operations/pkg.py +70 -0
- pyinfra/operations/pkgin.py +91 -0
- pyinfra/operations/postgres.py +436 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +40 -0
- pyinfra/operations/python.py +72 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +189 -0
- pyinfra/operations/server.py +1099 -0
- pyinfra/operations/snap.py +117 -0
- pyinfra/operations/ssh.py +216 -0
- pyinfra/operations/systemd.py +149 -0
- pyinfra/operations/sysvinit.py +141 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +251 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +336 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +77 -0
- pyinfra/operations/yum.py +210 -0
- pyinfra/operations/zfs.py +175 -0
- pyinfra/operations/zypper.py +192 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.5.1.dist-info/METADATA +141 -0
- pyinfra-3.5.1.dist-info/RECORD +159 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
- pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +780 -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/api/state.py
CHANGED
|
@@ -1,99 +1,214 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from graphlib import CycleError, TopologicalSorter
|
|
2
7
|
from multiprocessing import cpu_count
|
|
3
|
-
from
|
|
8
|
+
from typing import TYPE_CHECKING, Callable, Iterator, Optional
|
|
4
9
|
|
|
5
10
|
from gevent.pool import Pool
|
|
6
|
-
from
|
|
11
|
+
from paramiko import PKey
|
|
7
12
|
|
|
8
|
-
from pyinfra import
|
|
13
|
+
from pyinfra import logger
|
|
9
14
|
|
|
10
15
|
from .config import Config
|
|
11
16
|
from .exceptions import PyinfraError
|
|
12
|
-
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pyinfra.api.arguments import AllArguments
|
|
20
|
+
from pyinfra.api.command import PyinfraCommand
|
|
21
|
+
from pyinfra.api.host import Host
|
|
22
|
+
from pyinfra.api.inventory import Inventory
|
|
23
|
+
from pyinfra.api.operation import OperationMeta
|
|
24
|
+
|
|
13
25
|
|
|
14
26
|
# Work out the max parallel we can achieve with the open files limit of the user/process,
|
|
15
27
|
# take 10 for opening Python files and /3 for ~3 files per host during op runs.
|
|
16
28
|
# See: https://github.com/Fizzadar/pyinfra/issues/44
|
|
17
29
|
try:
|
|
18
|
-
from resource import
|
|
30
|
+
from resource import RLIMIT_NOFILE, getrlimit
|
|
31
|
+
|
|
19
32
|
nofile_limit, _ = getrlimit(RLIMIT_NOFILE)
|
|
20
33
|
MAX_PARALLEL = round((nofile_limit - 10) / 3)
|
|
21
34
|
|
|
22
35
|
# Resource isn't available on Windows
|
|
23
36
|
except ImportError:
|
|
24
37
|
nofile_limit = 0
|
|
25
|
-
MAX_PARALLEL =
|
|
38
|
+
MAX_PARALLEL = 100000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BaseStateCallback:
|
|
42
|
+
# Host callbacks
|
|
43
|
+
#
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def host_before_connect(state: "State", host: "Host"):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def host_connect(state: "State", host: "Host"):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def host_connect_error(state: "State", host: "Host", error):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def host_disconnect(state: "State", host: "Host"):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Operation callbacks
|
|
62
|
+
#
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def operation_start(state: "State", op_hash):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def operation_host_start(state: "State", host: "Host", op_hash):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def operation_host_success(state: "State", host: "Host", op_hash, retry_count: int = 0):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def operation_host_error(
|
|
78
|
+
state: "State", host: "Host", op_hash, retry_count: int = 0, max_retries: int = 0
|
|
79
|
+
):
|
|
80
|
+
pass
|
|
26
81
|
|
|
82
|
+
@staticmethod
|
|
83
|
+
def operation_host_retry(
|
|
84
|
+
state: "State", host: "Host", op_hash, retry_num: int, max_retries: int
|
|
85
|
+
):
|
|
86
|
+
pass
|
|
27
87
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
88
|
+
@staticmethod
|
|
89
|
+
def operation_end(state: "State", op_hash):
|
|
90
|
+
pass
|
|
31
91
|
|
|
32
|
-
Turn:
|
|
33
|
-
Deploy Kubernetes master/Configure Kubernetes
|
|
34
|
-
Into:
|
|
35
|
-
Deploy Kubernetes master/Configure
|
|
36
|
-
'''
|
|
37
92
|
|
|
38
|
-
|
|
39
|
-
|
|
93
|
+
class StateStage(IntEnum):
|
|
94
|
+
# Setup - collect inventory & data
|
|
95
|
+
Setup = 1
|
|
96
|
+
# Connect - connect to the inventory
|
|
97
|
+
Connect = 2
|
|
98
|
+
# Prepare - detect operation changes
|
|
99
|
+
Prepare = 3
|
|
100
|
+
# Execute - execute operations
|
|
101
|
+
Execute = 4
|
|
102
|
+
# Disconnect - disconnect from the inventory
|
|
103
|
+
Disconnect = 5
|
|
40
104
|
|
|
41
|
-
new = ' '.join(
|
|
42
|
-
new_token for new_token in new_tokens
|
|
43
|
-
if new_token not in current_tokens
|
|
44
|
-
)
|
|
45
105
|
|
|
46
|
-
|
|
106
|
+
class StateOperationMeta:
|
|
107
|
+
names: set[str]
|
|
108
|
+
args: list[str]
|
|
109
|
+
op_order: tuple[int, ...]
|
|
110
|
+
global_arguments: "AllArguments"
|
|
47
111
|
|
|
112
|
+
def __init__(self, op_order: tuple[int, ...]):
|
|
113
|
+
self.op_order = op_order
|
|
114
|
+
self.names = set()
|
|
115
|
+
self.args = []
|
|
116
|
+
self.global_arguments = {} # type: ignore
|
|
48
117
|
|
|
49
|
-
|
|
50
|
-
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class StateOperationHostData:
|
|
121
|
+
command_generator: Callable[[], Iterator["PyinfraCommand"]]
|
|
122
|
+
global_arguments: "AllArguments"
|
|
123
|
+
operation_meta: "OperationMeta"
|
|
124
|
+
parent_op_hash: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class StateHostMeta:
|
|
128
|
+
ops = 0
|
|
129
|
+
ops_change = 0
|
|
130
|
+
ops_no_change = 0
|
|
131
|
+
op_hashes: set[str]
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
self.op_hashes = set()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class StateHostResults:
|
|
138
|
+
ops = 0
|
|
139
|
+
success_ops = 0
|
|
140
|
+
error_ops = 0
|
|
141
|
+
ignored_error_ops = 0
|
|
142
|
+
partial_ops = 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class State:
|
|
146
|
+
"""
|
|
51
147
|
Manages state for a pyinfra deploy.
|
|
52
|
-
|
|
148
|
+
"""
|
|
53
149
|
|
|
54
|
-
initialised = False
|
|
150
|
+
initialised: bool = False
|
|
55
151
|
|
|
56
152
|
# A pyinfra.api.Inventory which stores all our pyinfra.api.Host's
|
|
57
|
-
inventory
|
|
153
|
+
inventory: "Inventory"
|
|
58
154
|
|
|
59
155
|
# A pyinfra.api.Config
|
|
60
|
-
config
|
|
156
|
+
config: "Config"
|
|
61
157
|
|
|
62
158
|
# Main gevent pool
|
|
63
|
-
pool
|
|
64
|
-
|
|
65
|
-
# Whether we are in an @operation (so inner ops aren't wrapped)
|
|
66
|
-
in_op = False
|
|
67
|
-
# Whether we are deploying (ie hosts are all ready)
|
|
68
|
-
deploying = False
|
|
159
|
+
pool: "Pool"
|
|
69
160
|
|
|
70
|
-
# Current
|
|
71
|
-
|
|
161
|
+
# Current stage this state is in
|
|
162
|
+
current_stage: StateStage = StateStage.Setup
|
|
163
|
+
# Warning counters by stage
|
|
164
|
+
stage_warnings: dict[StateStage, int] = defaultdict(int)
|
|
72
165
|
|
|
73
|
-
|
|
74
|
-
|
|
166
|
+
# Whether we are executing operations (ie hosts are all ready)
|
|
167
|
+
is_executing: bool = False
|
|
75
168
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
deploy_kwargs = None
|
|
80
|
-
deploy_data = None
|
|
81
|
-
deploy_line_numbers = None
|
|
169
|
+
# Whether we should check for operation changes as part of the operation ordering phase, this
|
|
170
|
+
# allows us to guesstimate which ops will result in changes on which hosts.
|
|
171
|
+
check_for_changes: bool = True
|
|
82
172
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
173
|
+
print_noop_info: bool = False # print "[host] noop: reason for noop"
|
|
174
|
+
print_fact_info: bool = False # print "loaded fact X"
|
|
175
|
+
print_input: bool = False
|
|
176
|
+
print_fact_input: bool = False
|
|
177
|
+
print_output: bool = False
|
|
178
|
+
print_fact_output: bool = False
|
|
87
179
|
|
|
88
180
|
# Used in CLI
|
|
89
|
-
|
|
90
|
-
|
|
181
|
+
cwd: Optional[str] = None # base directory for locating files/templates/etc
|
|
182
|
+
current_deploy_filename: Optional[str] = None
|
|
183
|
+
current_exec_filename: Optional[str] = None
|
|
184
|
+
current_op_file_number: int = 0
|
|
185
|
+
should_raise_failed_hosts: Optional[Callable[["State"], bool]] = None
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
inventory: Optional["Inventory"] = None,
|
|
190
|
+
config: Optional["Config"] = None,
|
|
191
|
+
check_for_changes: bool = True,
|
|
192
|
+
**kwargs,
|
|
193
|
+
):
|
|
194
|
+
"""
|
|
195
|
+
Initializes the state, the main Pyinfra
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
inventory (Optional[Inventory], optional): The inventory. Defaults to None.
|
|
199
|
+
config (Optional[Config], optional): The config object. Defaults to None.
|
|
200
|
+
"""
|
|
201
|
+
self.check_for_changes = check_for_changes
|
|
91
202
|
|
|
92
|
-
def __init__(self, inventory=None, config=None, **kwargs):
|
|
93
203
|
if inventory:
|
|
94
204
|
self.init(inventory, config, **kwargs)
|
|
95
205
|
|
|
96
|
-
def init(
|
|
206
|
+
def init(
|
|
207
|
+
self,
|
|
208
|
+
inventory: "Inventory",
|
|
209
|
+
config: Optional["Config"],
|
|
210
|
+
initial_limit=None,
|
|
211
|
+
):
|
|
97
212
|
# Config validation
|
|
98
213
|
#
|
|
99
214
|
|
|
@@ -101,23 +216,6 @@ class State(object):
|
|
|
101
216
|
if config is None:
|
|
102
217
|
config = Config()
|
|
103
218
|
|
|
104
|
-
# Error if our min version is not met
|
|
105
|
-
if config.MIN_PYINFRA_VERSION is not None:
|
|
106
|
-
running_version = parse_version(__version__)
|
|
107
|
-
needed_version = parse_version(
|
|
108
|
-
# Version must be a string
|
|
109
|
-
str(config.MIN_PYINFRA_VERSION),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
if needed_version > running_version:
|
|
113
|
-
raise PyinfraError((
|
|
114
|
-
'Minimum pyinfra version not met '
|
|
115
|
-
'(minimum={0}, running={1})'
|
|
116
|
-
).format(
|
|
117
|
-
config.MIN_PYINFRA_VERSION,
|
|
118
|
-
__version__,
|
|
119
|
-
))
|
|
120
|
-
|
|
121
219
|
if not config.PARALLEL:
|
|
122
220
|
# TODO: benchmark this
|
|
123
221
|
# In my own tests the optimum number of parallel SSH processes is
|
|
@@ -125,215 +223,182 @@ class State(object):
|
|
|
125
223
|
cpus = cpu_count()
|
|
126
224
|
ideal_parallel = cpus * 20
|
|
127
225
|
|
|
128
|
-
config.PARALLEL = (
|
|
129
|
-
min(ideal_parallel, len(inventory), MAX_PARALLEL)
|
|
130
|
-
if MAX_PARALLEL is not None
|
|
131
|
-
else min(ideal_parallel, len(inventory))
|
|
132
|
-
)
|
|
226
|
+
config.PARALLEL = min(ideal_parallel, len(inventory), MAX_PARALLEL)
|
|
133
227
|
|
|
134
228
|
# If explicitly set, just issue a warning
|
|
135
|
-
elif
|
|
136
|
-
logger.warning(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
229
|
+
elif config.PARALLEL > MAX_PARALLEL:
|
|
230
|
+
logger.warning(
|
|
231
|
+
(
|
|
232
|
+
"Parallel set to {0}, but this may hit the open files limit of {1}.\n"
|
|
233
|
+
" Max recommended value: {2}"
|
|
234
|
+
).format(config.PARALLEL, nofile_limit, MAX_PARALLEL),
|
|
235
|
+
)
|
|
140
236
|
|
|
141
237
|
# Actually initialise the state object
|
|
142
238
|
#
|
|
143
239
|
|
|
240
|
+
self.callback_handlers: list[BaseStateCallback] = []
|
|
241
|
+
|
|
144
242
|
# Setup greenlet pools
|
|
145
243
|
self.pool = Pool(config.PARALLEL)
|
|
146
244
|
self.fact_pool = Pool(config.PARALLEL)
|
|
147
245
|
|
|
148
|
-
# Connection storage
|
|
149
|
-
self.ssh_connections = {}
|
|
150
|
-
self.sftp_connections = {}
|
|
151
|
-
|
|
152
246
|
# Private keys
|
|
153
|
-
self.private_keys = {}
|
|
154
|
-
|
|
155
|
-
# Facts storage
|
|
156
|
-
self.facts = {}
|
|
157
|
-
self.fact_locks = {}
|
|
247
|
+
self.private_keys: dict[str, PKey] = {}
|
|
158
248
|
|
|
159
249
|
# Assign inventory/config
|
|
160
250
|
self.inventory = inventory
|
|
161
251
|
self.config = config
|
|
162
252
|
|
|
163
253
|
# Hosts we've activated at any time
|
|
164
|
-
self.activated_hosts = set()
|
|
254
|
+
self.activated_hosts: set["Host"] = set()
|
|
165
255
|
# Active hosts that *haven't* failed yet
|
|
166
|
-
self.active_hosts = set()
|
|
167
|
-
# Hosts that
|
|
168
|
-
self.
|
|
256
|
+
self.active_hosts: set["Host"] = set()
|
|
257
|
+
# Hosts that have failed
|
|
258
|
+
self.failed_hosts: set["Host"] = set()
|
|
169
259
|
|
|
170
260
|
# Limit hosts changes dynamically to limit operations to a subset of hosts
|
|
171
|
-
self.limit_hosts = initial_limit
|
|
261
|
+
self.limit_hosts: list["Host"] = initial_limit
|
|
172
262
|
|
|
173
263
|
# Op basics
|
|
174
|
-
self.
|
|
175
|
-
self.op_meta = {} # maps operation hash -> names/etc
|
|
176
|
-
self.ops_run = set() # list of ops which have been started/run
|
|
264
|
+
self.op_meta: dict[str, StateOperationMeta] = {} # maps operation hash -> names/etc
|
|
177
265
|
|
|
178
266
|
# Op dict for each host
|
|
179
|
-
self.ops = {
|
|
180
|
-
host: {}
|
|
181
|
-
for host in inventory
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
# Facts dict for each host
|
|
185
|
-
self.facts = {
|
|
186
|
-
host: {}
|
|
187
|
-
for host in inventory
|
|
188
|
-
}
|
|
267
|
+
self.ops: dict["Host", dict[str, StateOperationHostData]] = {host: {} for host in inventory}
|
|
189
268
|
|
|
190
269
|
# Meta dict for each host
|
|
191
|
-
self.meta = {
|
|
192
|
-
host: {
|
|
193
|
-
'ops': 0, # one function call in a deploy file
|
|
194
|
-
'commands': 0, # actual # of commands to run
|
|
195
|
-
'op_hashes': set(),
|
|
196
|
-
}
|
|
197
|
-
for host in inventory
|
|
198
|
-
}
|
|
270
|
+
self.meta: dict["Host", StateHostMeta] = {host: StateHostMeta() for host in inventory}
|
|
199
271
|
|
|
200
272
|
# Results dict for each host
|
|
201
|
-
self.results = {
|
|
202
|
-
host:
|
|
203
|
-
'ops': 0, # success_ops + failed ops w/ignore_errors
|
|
204
|
-
'success_ops': 0,
|
|
205
|
-
'error_ops': 0,
|
|
206
|
-
'commands': 0,
|
|
207
|
-
}
|
|
208
|
-
for host in inventory
|
|
273
|
+
self.results: dict["Host", StateHostResults] = {
|
|
274
|
+
host: StateHostResults() for host in inventory
|
|
209
275
|
}
|
|
210
276
|
|
|
211
277
|
# Assign state back references to inventory & config
|
|
212
278
|
inventory.state = config.state = self
|
|
279
|
+
for host in inventory:
|
|
280
|
+
host.init(self)
|
|
213
281
|
|
|
214
282
|
self.initialised = True
|
|
215
283
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if 'hosts' in kwargs:
|
|
242
|
-
kwargs['hosts'] = [
|
|
243
|
-
host for host in kwargs['hosts']
|
|
244
|
-
if host in old_deploy_kwargs['hosts']
|
|
245
|
-
]
|
|
246
|
-
# Otherwise simply carry the previous hosts
|
|
247
|
-
else:
|
|
248
|
-
kwargs['hosts'] = old_deploy_kwargs['hosts']
|
|
249
|
-
|
|
250
|
-
# Make new line numbers - note convert from and back to tuple to avoid
|
|
251
|
-
# keeping deploy_line_numbers mutable.
|
|
252
|
-
new_line_numbers = list(self.deploy_line_numbers or [])
|
|
253
|
-
new_line_numbers.append(line_number)
|
|
254
|
-
new_line_numbers = tuple(new_line_numbers)
|
|
255
|
-
|
|
256
|
-
# Set the new values
|
|
257
|
-
self.deploy_name = name
|
|
258
|
-
self.deploy_kwargs = kwargs
|
|
259
|
-
self.deploy_data = data
|
|
260
|
-
self.deploy_line_numbers = new_line_numbers
|
|
261
|
-
logger.debug('Starting deploy {0} (args={1}, data={2})'.format(
|
|
262
|
-
name, kwargs, data,
|
|
263
|
-
))
|
|
264
|
-
|
|
265
|
-
yield
|
|
266
|
-
|
|
267
|
-
# Restore the previous values
|
|
268
|
-
self.in_deploy = old_in_deploy
|
|
269
|
-
self.deploy_name = old_deploy_name
|
|
270
|
-
self.deploy_kwargs = old_deploy_kwargs
|
|
271
|
-
self.deploy_data = old_deploy_data
|
|
272
|
-
self.deploy_line_numbers = old_deploy_line_numbers
|
|
273
|
-
|
|
274
|
-
logger.debug('Reset deploy to {0} (args={1}, data={2})'.format(
|
|
275
|
-
old_deploy_name, old_deploy_kwargs, old_deploy_data,
|
|
276
|
-
))
|
|
277
|
-
|
|
278
|
-
@contextmanager
|
|
279
|
-
def preserve_loop_order(self, items):
|
|
280
|
-
frameinfo = get_caller_frameinfo(frame_offset=1) # escape contextlib
|
|
281
|
-
self.loop_line = frameinfo.lineno
|
|
282
|
-
|
|
283
|
-
def item_generator():
|
|
284
|
-
for i, item in enumerate(items, 1):
|
|
285
|
-
self.loop_counter = i
|
|
286
|
-
yield item
|
|
287
|
-
|
|
288
|
-
yield item_generator
|
|
289
|
-
|
|
290
|
-
self.loop_counter = None
|
|
291
|
-
self.loop_line = None
|
|
284
|
+
def set_stage(self, stage: StateStage) -> None:
|
|
285
|
+
if stage < self.current_stage:
|
|
286
|
+
raise Exception("State stage cannot go backwards!")
|
|
287
|
+
self.current_stage = stage
|
|
288
|
+
|
|
289
|
+
def increment_warning_counter(self) -> None:
|
|
290
|
+
self.stage_warnings[self.current_stage] += 1
|
|
291
|
+
|
|
292
|
+
def get_warning_counter(self) -> int:
|
|
293
|
+
return self.stage_warnings[self.current_stage]
|
|
294
|
+
|
|
295
|
+
def should_check_for_changes(self):
|
|
296
|
+
return self.check_for_changes
|
|
297
|
+
|
|
298
|
+
def add_callback_handler(self, handler):
|
|
299
|
+
if not isinstance(handler, BaseStateCallback):
|
|
300
|
+
raise TypeError(
|
|
301
|
+
("{0} is not a valid callback handler (use `BaseStateCallback`)").format(handler),
|
|
302
|
+
)
|
|
303
|
+
self.callback_handlers.append(handler)
|
|
304
|
+
|
|
305
|
+
def trigger_callbacks(self, method_name: str, *args, **kwargs):
|
|
306
|
+
for handler in self.callback_handlers:
|
|
307
|
+
func = getattr(handler, method_name)
|
|
308
|
+
func(self, *args, **kwargs)
|
|
292
309
|
|
|
293
310
|
def get_op_order(self):
|
|
294
|
-
|
|
295
|
-
|
|
311
|
+
ts: TopologicalSorter = TopologicalSorter()
|
|
312
|
+
|
|
313
|
+
for host in self.inventory:
|
|
314
|
+
for i, op_hash in enumerate(host.op_hash_order):
|
|
315
|
+
if not i:
|
|
316
|
+
ts.add(op_hash)
|
|
317
|
+
else:
|
|
318
|
+
ts.add(op_hash, host.op_hash_order[i - 1])
|
|
319
|
+
|
|
320
|
+
final_op_order = []
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
ts.prepare()
|
|
324
|
+
except CycleError as e:
|
|
325
|
+
raise PyinfraError(
|
|
326
|
+
(
|
|
327
|
+
"Cycle detected in operation ordering DAG.\n"
|
|
328
|
+
f" Error: {e}\n\n"
|
|
329
|
+
" This can happen when using loops in operation code, "
|
|
330
|
+
"please see: https://docs.pyinfra.com/en/latest/deploy-process.html#loops-cycle-errors" # noqa: E501
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
while ts.is_active():
|
|
335
|
+
# Ensure that where we have multiple different operations that can be executed in any
|
|
336
|
+
# dependency order we order them by line numbers.
|
|
337
|
+
node_group = sorted(
|
|
338
|
+
ts.get_ready(),
|
|
339
|
+
key=lambda op_hash: self.op_meta[op_hash].op_order,
|
|
340
|
+
)
|
|
341
|
+
ts.done(*node_group)
|
|
342
|
+
final_op_order.extend(node_group)
|
|
343
|
+
|
|
344
|
+
return final_op_order
|
|
296
345
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
for numbers in sorted_line_numbers
|
|
300
|
-
]
|
|
346
|
+
def get_op_meta(self, op_hash: str) -> StateOperationMeta:
|
|
347
|
+
return self.op_meta[op_hash]
|
|
301
348
|
|
|
302
|
-
def
|
|
303
|
-
|
|
349
|
+
def get_meta_for_host(self, host: "Host") -> StateHostMeta:
|
|
350
|
+
return self.meta[host]
|
|
351
|
+
|
|
352
|
+
def get_results_for_host(self, host: "Host") -> StateHostResults:
|
|
353
|
+
return self.results[host]
|
|
354
|
+
|
|
355
|
+
def get_op_data_for_host(
|
|
356
|
+
self,
|
|
357
|
+
host: "Host",
|
|
358
|
+
op_hash: str,
|
|
359
|
+
) -> StateOperationHostData:
|
|
360
|
+
return self.ops[host][op_hash]
|
|
361
|
+
|
|
362
|
+
def set_op_data_for_host(
|
|
363
|
+
self,
|
|
364
|
+
host: "Host",
|
|
365
|
+
op_hash: str,
|
|
366
|
+
op_data: StateOperationHostData,
|
|
367
|
+
):
|
|
368
|
+
self.ops[host][op_hash] = op_data
|
|
369
|
+
|
|
370
|
+
def activate_host(self, host: "Host"):
|
|
371
|
+
"""
|
|
304
372
|
Flag a host as active.
|
|
305
|
-
|
|
373
|
+
"""
|
|
306
374
|
|
|
307
|
-
logger.debug(
|
|
375
|
+
logger.debug("Activating host: %s", host)
|
|
308
376
|
|
|
309
377
|
# Add to *both* activated and active - active will reduce as hosts fail
|
|
310
378
|
# but connected will not, enabling us to track failed %.
|
|
311
379
|
self.activated_hosts.add(host)
|
|
312
380
|
self.active_hosts.add(host)
|
|
313
381
|
|
|
314
|
-
# def ready_host(self, host):
|
|
315
|
-
# '''
|
|
316
|
-
# Flag a host as ready, after which facts will not be gathered for it.
|
|
317
|
-
# '''
|
|
318
|
-
|
|
319
|
-
# logger.debug('Readying host: {0}'.format(host))
|
|
320
|
-
# self.ready_hosts.add(host)
|
|
321
|
-
|
|
322
382
|
def fail_hosts(self, hosts_to_fail, activated_count=None):
|
|
323
|
-
|
|
383
|
+
"""
|
|
324
384
|
Flag a ``set`` of hosts as failed, error for ``config.FAIL_PERCENT``.
|
|
325
|
-
|
|
385
|
+
"""
|
|
326
386
|
|
|
327
387
|
if not hosts_to_fail:
|
|
328
388
|
return
|
|
329
389
|
|
|
330
390
|
activated_count = activated_count or len(self.activated_hosts)
|
|
331
391
|
|
|
332
|
-
logger.debug(
|
|
333
|
-
|
|
334
|
-
|
|
392
|
+
logger.debug(
|
|
393
|
+
"Failing hosts: {0}".format(
|
|
394
|
+
", ".join(
|
|
395
|
+
(host.name for host in hosts_to_fail),
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
self.failed_hosts.update(hosts_to_fail)
|
|
335
401
|
|
|
336
|
-
# Remove the failed hosts from the inventory
|
|
337
402
|
self.active_hosts -= hosts_to_fail
|
|
338
403
|
|
|
339
404
|
# Check we're not above the fail percent
|
|
@@ -341,40 +406,29 @@ class State(object):
|
|
|
341
406
|
|
|
342
407
|
# No hosts left!
|
|
343
408
|
if not active_hosts:
|
|
344
|
-
raise PyinfraError(
|
|
409
|
+
raise PyinfraError("No hosts remaining!")
|
|
345
410
|
|
|
346
411
|
if self.config.FAIL_PERCENT is not None:
|
|
347
|
-
percent_failed = (
|
|
348
|
-
1 - len(active_hosts) / activated_count
|
|
349
|
-
) * 100
|
|
412
|
+
percent_failed = (1 - len(active_hosts) / activated_count) * 100
|
|
350
413
|
|
|
351
414
|
if percent_failed > self.config.FAIL_PERCENT:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
415
|
+
if self.should_raise_failed_hosts and self.should_raise_failed_hosts(self) is False:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
raise PyinfraError(
|
|
419
|
+
"Over {0}% of hosts failed ({1}%)".format(
|
|
420
|
+
self.config.FAIL_PERCENT,
|
|
421
|
+
int(round(percent_failed)),
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def is_host_in_limit(self, host: "Host"):
|
|
426
|
+
"""
|
|
359
427
|
Returns a boolean indicating if the host is within the current state limit.
|
|
360
|
-
|
|
428
|
+
"""
|
|
361
429
|
|
|
362
430
|
limit_hosts = self.limit_hosts
|
|
363
431
|
|
|
364
432
|
if not isinstance(limit_hosts, list):
|
|
365
433
|
return True
|
|
366
434
|
return host in limit_hosts
|
|
367
|
-
|
|
368
|
-
def get_temp_filename(self, hash_key=None):
|
|
369
|
-
'''
|
|
370
|
-
Generate a temporary filename for this deploy.
|
|
371
|
-
'''
|
|
372
|
-
|
|
373
|
-
if not hash_key:
|
|
374
|
-
hash_key = str(uuid4())
|
|
375
|
-
|
|
376
|
-
temp_filename = '{0}/{1}'.format(
|
|
377
|
-
self.config.TEMP_DIR, sha1_hash(hash_key),
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
return temp_filename
|