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.
Files changed (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/api/state.py CHANGED
@@ -1,99 +1,214 @@
1
- from contextlib import contextmanager
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 uuid import uuid4
8
+ from typing import TYPE_CHECKING, Callable, Iterator, Optional
4
9
 
5
10
  from gevent.pool import Pool
6
- from pkg_resources import parse_version
11
+ from paramiko import PKey
7
12
 
8
- from pyinfra import __version__, logger
13
+ from pyinfra import logger
9
14
 
10
15
  from .config import Config
11
16
  from .exceptions import PyinfraError
12
- from .util import get_caller_frameinfo, sha1_hash
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 getrlimit, RLIMIT_NOFILE
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 = None
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
- def _make_name(current, new):
29
- '''
30
- Stops duplication between similarly named nested deploys, eg:
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
- current_tokens = current.split()
39
- new_tokens = new.split()
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
- return '/'.join((current, new))
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
- class State(object):
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 = None
153
+ inventory: "Inventory"
58
154
 
59
155
  # A pyinfra.api.Config
60
- config = None
156
+ config: "Config"
61
157
 
62
158
  # Main gevent pool
63
- pool = None
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 op hash for use w/facts
71
- current_op_hash = None
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
- loop_counter = None
74
- loop_line = None
166
+ # Whether we are executing operations (ie hosts are all ready)
167
+ is_executing: bool = False
75
168
 
76
- # Name of the current deploy
77
- in_deploy = False
78
- deploy_name = None
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
- # Flags for printing
84
- print_output = False # print output from the actual deploy (-v)
85
- print_fact_info = False # log fact gathering as INFO > DEBUG (-v)
86
- print_fact_output = False # print output from facts (-vv)
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
- deploy_dir = None # base directory for locating files/templates/etc
90
- current_op_file = 0 # increments for each file excuted to maintain order
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(self, inventory, config, initial_limit=None):
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 MAX_PARALLEL is not None and config.PARALLEL > MAX_PARALLEL:
136
- logger.warning((
137
- 'Parallel set to {0}, but this may hit the open files limit of {1}.\n'
138
- ' Max recommended value: {2}'
139
- ).format(config.PARALLEL, nofile_limit, MAX_PARALLEL))
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 are ready to be deployed to
168
- self.ready_hosts = set()
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.op_line_numbers_to_hash = {}
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
- @contextmanager
217
- def deploy(self, name, kwargs, data, line_number, in_deploy=True):
218
- '''
219
- Wraps a group of operations as a deploy, this should not be used
220
- directly, instead use ``pyinfra.api.deploy.deploy``.
221
- '''
222
-
223
- # Handle nested deploy names
224
- if self.deploy_name:
225
- name = _make_name(self.deploy_name, name)
226
-
227
- # Store the previous values
228
- old_in_deploy = self.in_deploy
229
- old_deploy_name = self.deploy_name
230
- old_deploy_kwargs = self.deploy_kwargs
231
- old_deploy_data = self.deploy_data
232
- old_deploy_line_numbers = self.deploy_line_numbers
233
- self.in_deploy = in_deploy
234
-
235
- # Limit the new hosts to a subset of the old hosts if they existed
236
- if (
237
- old_deploy_kwargs
238
- and old_deploy_kwargs.get('hosts') is not None
239
- ):
240
- # If we have hosts - subset them based on the old hosts
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
- line_numbers_to_hash = self.op_line_numbers_to_hash
295
- sorted_line_numbers = sorted(list(line_numbers_to_hash.keys()))
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
- return [
298
- line_numbers_to_hash[numbers]
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 activate_host(self, host):
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('Activating host: {0}'.format(host))
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('Failing hosts: {0}'.format(', '.join(
333
- (host.name for host in hosts_to_fail),
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('No hosts remaining!')
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
- raise PyinfraError('Over {0}% of hosts failed ({1}%)'.format(
353
- self.config.FAIL_PERCENT,
354
- int(round(percent_failed)),
355
- ))
356
-
357
- def is_host_in_limit(self, host):
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