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/modules/files.py DELETED
@@ -1,711 +0,0 @@
1
- '''
2
- The files module handles filesystem state, file uploads and template generation.
3
- '''
4
-
5
- import posixpath
6
- import sys
7
-
8
- from datetime import timedelta
9
- from fnmatch import fnmatch
10
- from io import StringIO
11
- from os import makedirs, path, walk
12
-
13
- from jinja2 import TemplateSyntaxError, UndefinedError
14
-
15
- from pyinfra.api import operation, OperationError
16
- from pyinfra.api.util import get_file_sha1, get_template
17
-
18
- from .util.files import chmod, chown, ensure_mode_int, sed_replace
19
-
20
-
21
- @operation(pipeline_facts={
22
- 'file': 'destination',
23
- })
24
- def download(
25
- state, host, source_url, destination,
26
- user=None, group=None, mode=None, cache_time=None, force=False,
27
- ):
28
- '''
29
- Download files from remote locations.
30
-
31
- + source_url: source URl of the file
32
- + destination: where to save the file
33
- + user: user to own the files
34
- + group: group to own the files
35
- + mode: permissions of the files
36
- + cache_time: if the file exists already, re-download after this time (in s)
37
- + force: always download the file, even if it already exists
38
- '''
39
-
40
- # Get destination info
41
- info = host.fact.file(destination)
42
-
43
- # Destination is a directory?
44
- if info is False:
45
- raise OperationError(
46
- 'Destination {0} already exists and is not a file'.format(destination),
47
- )
48
-
49
- # Do we download the file? Force by default
50
- download = force
51
-
52
- # Doesn't exist, lets download it
53
- if info is None:
54
- download = True
55
-
56
- # Destination file exists & cache_time: check when the file was last modified,
57
- # download if old
58
- elif cache_time:
59
- # Time on files is not tz-aware, and will be the same tz as the server's time,
60
- # so we can safely remove the tzinfo from host.fact.date before comparison.
61
- cache_time = host.fact.date.replace(tzinfo=None) - timedelta(seconds=cache_time)
62
- if info['mtime'] and info['mtime'] > cache_time:
63
- download = True
64
-
65
- # If we download, always do user/group/mode as SSH user may be different
66
- if download:
67
- yield 'wget -q {0} -O {1}'.format(source_url, destination)
68
-
69
- if user or group:
70
- yield chown(destination, user, group)
71
-
72
- if mode:
73
- yield chmod(destination, mode)
74
-
75
-
76
- @operation
77
- def line(state, host, name, line, present=True, replace=None, flags=None):
78
- '''
79
- Ensure lines in files using grep to locate and sed to replace.
80
-
81
- + name: target remote file to edit
82
- + line: string or regex matching the target line
83
- + present: whether the line should be in the file
84
- + replace: text to replace entire matching lines when ``present=True``
85
- + flags: list of flags to pass to sed when replacing/deleting
86
-
87
- Regex line matching:
88
- Unless line matches a line (starts with ^, ends $), pyinfra will wrap it such that
89
- it does, like: ``^.*LINE.*$``. This means we don't swap parts of lines out. To
90
- change bits of lines, see ``files.replace``.
91
-
92
- Regex line escaping:
93
- If matching special characters (eg a crontab line containing *), remember to escape
94
- it first using Python's ``re.escape``.
95
- '''
96
-
97
- match_line = line
98
-
99
- # Ensure we're matching a whole ^line$
100
- if not match_line.startswith('^'):
101
- match_line = '^.*{0}'.format(match_line)
102
-
103
- if not match_line.endswith('$'):
104
- match_line = '{0}.*$'.format(match_line)
105
-
106
- # Is there a matching line in this file?
107
- present_lines = host.fact.find_in_file(name, match_line)
108
-
109
- # If replace present, use that over the matching line
110
- if replace:
111
- line = replace
112
- # We must provide some kind of replace to sed_replace_command below
113
- else:
114
- replace = ''
115
-
116
- # Save commands for re-use in dynamic script when file not present at fact stage
117
- echo_command = 'echo "{0}" >> {1}'.format(line, name)
118
- sed_replace_command = sed_replace(
119
- name, match_line, replace,
120
- flags=flags,
121
- )
122
-
123
- # No line and we want it, append it
124
- if not present_lines and present:
125
- # If the file does not exist - it *might* be created, so we handle it
126
- # dynamically with a little script.
127
- if present_lines is None:
128
- yield '''
129
- # If the file now exists
130
- if [ -f "{target}" ]; then
131
- # Grep for the line, sed if matches
132
- (grep "{match_line}" "{target}" && {sed_replace_command}) || \
133
- # Else echo
134
- {echo_command}
135
-
136
- # No file, just echo
137
- else
138
- {echo_command}
139
- fi
140
- '''.format(
141
- target=name,
142
- match_line=match_line,
143
- echo_command=echo_command,
144
- sed_replace_command=sed_replace_command,
145
- )
146
-
147
- # Otherwise the file exists and there is no matching line, so append it
148
- else:
149
- yield echo_command
150
-
151
- # Line(s) exists and we want to remove them, replace with nothing
152
- elif present_lines and not present:
153
- yield sed_replace(name, match_line, '', flags=flags)
154
-
155
- # Line(s) exists and we have want to ensure they're correct
156
- elif present_lines and present:
157
- # If any of lines are different, sed replace them
158
- if replace and any(line != replace for line in present_lines):
159
- yield sed_replace_command
160
-
161
-
162
- @operation
163
- def replace(state, host, name, match, replace, flags=None):
164
- '''
165
- A simple shortcut for replacing text in files with sed.
166
-
167
- + name: target remote file to edit
168
- + match: text/regex to match for
169
- + replace: text to replace with
170
- + flags: list of flaggs to pass to sed
171
- '''
172
-
173
- yield sed_replace(name, match, replace, flags=flags)
174
-
175
-
176
- @operation(pipeline_facts={
177
- 'find_files': 'destination',
178
- })
179
- def sync(
180
- state, host, source, destination,
181
- user=None, group=None, mode=None, delete=False,
182
- exclude=None, exclude_dir=None, add_deploy_dir=True,
183
- ):
184
- '''
185
- Syncs a local directory with a remote one, with delete support. Note that delete will
186
- remove extra files on the remote side, but not extra directories.
187
-
188
- + source: local directory to sync
189
- + destination: remote directory to sync to
190
- + user: user to own the files and directories
191
- + group: group to own the files and directories
192
- + mode: permissions of the files
193
- + delete: delete remote files not present locally
194
- + exclude: string or list/tuple of strings to match & exclude files (eg *.pyc)
195
- + exclude_dir: string or list/tuple of strings to match & exclude directories (eg node_modules)
196
- '''
197
-
198
- # If we don't enforce the source ending with /, remote_dirname below might start with
199
- # a /, which makes the path.join cut off the destination bit.
200
- if not source.endswith(path.sep):
201
- source = '{0}{1}'.format(source, path.sep)
202
-
203
- # Add deploy directory?
204
- if add_deploy_dir and state.deploy_dir:
205
- source = path.join(state.deploy_dir, source)
206
-
207
- # Ensure the source directory exists
208
- if not path.isdir(source):
209
- raise IOError('No such directory: {0}'.format(source))
210
-
211
- # Ensure exclude is a list/tuple
212
- if exclude is not None:
213
- if not isinstance(exclude, (list, tuple)):
214
- exclude = [exclude]
215
-
216
- # Ensure exclude_dir is a list/tuple
217
- if exclude_dir is not None:
218
- if not isinstance(exclude_dir, (list, tuple)):
219
- exclude_dir = [exclude_dir]
220
-
221
- put_files = []
222
- ensure_dirnames = []
223
- for dirname, _, filenames in walk(source):
224
- remote_dirname = dirname.replace(source, '')
225
-
226
- # Should we exclude this dir?
227
- if exclude_dir and any(fnmatch(remote_dirname, match) for match in exclude_dir):
228
- continue
229
-
230
- if remote_dirname:
231
- ensure_dirnames.append(remote_dirname)
232
-
233
- for filename in filenames:
234
- full_filename = path.join(dirname, filename)
235
-
236
- # Should we exclude this file?
237
- if exclude and any(fnmatch(full_filename, match) for match in exclude):
238
- continue
239
-
240
- put_files.append((
241
- # Join local as normal (unix, win)
242
- full_filename,
243
- # Join remote as unix like
244
- '/'.join(
245
- item for item in
246
- (destination, remote_dirname, filename)
247
- if item
248
- ),
249
- ))
250
-
251
- # Ensure the destination directory
252
- yield directory(
253
- state, host, destination,
254
- user=user, group=group,
255
- )
256
-
257
- # Ensure any remote dirnames
258
- for dirname in ensure_dirnames:
259
- yield directory(
260
- state, host,
261
- '/'.join((destination, dirname)),
262
- user=user, group=group,
263
- )
264
-
265
- # Put each file combination
266
- for local_filename, remote_filename in put_files:
267
- yield put(
268
- state, host,
269
- local_filename, remote_filename,
270
- user=user, group=group, mode=mode,
271
- add_deploy_dir=False,
272
- )
273
-
274
- # Delete any extra files
275
- if delete:
276
- remote_filenames = set(host.fact.find_files(destination) or [])
277
- wanted_filenames = set([remote_filename for _, remote_filename in put_files])
278
- files_to_delete = remote_filenames - wanted_filenames
279
- for filename in files_to_delete:
280
- # Should we exclude this file?
281
- if exclude and any(fnmatch(filename, match) for match in exclude):
282
- continue
283
-
284
- yield file(state, host, filename, present=False)
285
-
286
-
287
- def _create_remote_dir(state, host, remote_filename, user, group):
288
- # Always use POSIX style path as local might be Windows, remote always *nix
289
- remote_dirname = posixpath.dirname(remote_filename)
290
- if remote_dirname:
291
- yield directory(
292
- state, host, remote_dirname,
293
- user=user, group=group,
294
- )
295
-
296
-
297
- @operation(pipeline_facts={
298
- 'file': 'remote_filename',
299
- 'sha1_file': 'remote_filename',
300
- })
301
- def get(
302
- state, host, remote_filename, local_filename,
303
- add_deploy_dir=True, create_local_dir=False, force=False,
304
- ):
305
- '''
306
- Download a file from the remote system.
307
-
308
- + remote_filename: the remote filename to download
309
- + local_filename: the local filename to download the file to
310
- + add_deploy_dir: local_filename is relative to the deploy directory
311
- + create_local_dir: create the local directory if it doesn't exist
312
- + force: always download the file, even if the local copy matches
313
-
314
- Note:
315
- This operation is not suitable for large files as it may involve copying
316
- the remote file before downloading it.
317
- '''
318
-
319
- if add_deploy_dir and state.deploy_dir:
320
- local_filename = path.join(state.deploy_dir, local_filename)
321
-
322
- if create_local_dir:
323
- local_pathname = path.dirname(local_filename)
324
- if not path.exists(local_pathname):
325
- makedirs(local_pathname)
326
-
327
- remote_file = host.fact.file(remote_filename)
328
-
329
- # No remote file, so assume exists and download it "blind"
330
- if not remote_file or force:
331
- yield ('download', remote_filename, local_filename)
332
-
333
- # Remote file exists - check if it matches our local
334
- else:
335
- local_sum = get_file_sha1(local_filename)
336
- remote_sum = host.fact.sha1_file(remote_filename)
337
-
338
- # Check sha1sum, upload if needed
339
- if local_sum != remote_sum:
340
- yield ('download', remote_filename, local_filename)
341
-
342
-
343
- @operation(pipeline_facts={
344
- 'file': 'remote_filename',
345
- 'sha1_file': 'remote_filename',
346
- })
347
- def put(
348
- state, host, local_filename, remote_filename,
349
- user=None, group=None, mode=None, add_deploy_dir=True,
350
- create_remote_dir=False, force=False,
351
- ):
352
- '''
353
- Upload a local file to the remote system.
354
-
355
- + local_filename: local filename
356
- + remote_filename: remote filename
357
- + user: user to own the files
358
- + group: group to own the files
359
- + mode: permissions of the files
360
- + add_deploy_dir: local_filename is relative to the deploy directory
361
- + create_remote_dir: create the remote directory if it doesn't exist
362
- + force: always upload the file, even if the remote copy matches
363
-
364
- ``create_remote_dir``:
365
- If the remote directory does not exist it will be created using the same
366
- user & group as passed to ``files.put``. The mode will *not* be copied over,
367
- if this is required call ``files.directory`` separately.
368
-
369
- Note:
370
- This operation is not suitable for large files as it may involve copying
371
- the file before uploading it.
372
- '''
373
-
374
- # Upload IO objects as-is
375
- if hasattr(local_filename, 'read'):
376
- local_file = local_filename
377
-
378
- # Assume string filename
379
- else:
380
- # Add deploy directory?
381
- if add_deploy_dir and state.deploy_dir:
382
- local_filename = path.join(state.deploy_dir, local_filename)
383
-
384
- local_file = local_filename
385
-
386
- if not path.isfile(local_file):
387
- raise IOError('No such file: {0}'.format(local_file))
388
-
389
- mode = ensure_mode_int(mode)
390
- remote_file = host.fact.file(remote_filename)
391
-
392
- if create_remote_dir:
393
- yield _create_remote_dir(state, host, remote_filename, user, group)
394
-
395
- # No remote file, always upload and user/group/mode if supplied
396
- if not remote_file or force:
397
- yield ('upload', local_file, remote_filename)
398
-
399
- if user or group:
400
- yield chown(remote_filename, user, group)
401
-
402
- if mode:
403
- yield chmod(remote_filename, mode)
404
-
405
- # File exists, check sum and check user/group/mode if supplied
406
- else:
407
- local_sum = get_file_sha1(local_filename)
408
- remote_sum = host.fact.sha1_file(remote_filename)
409
-
410
- # Check sha1sum, upload if needed
411
- if local_sum != remote_sum:
412
- yield ('upload', local_file, remote_filename)
413
-
414
- if user or group:
415
- yield chown(remote_filename, user, group)
416
-
417
- if mode:
418
- yield chmod(remote_filename, mode)
419
-
420
- else:
421
- # Check mode
422
- if mode and remote_file['mode'] != mode:
423
- yield chmod(remote_filename, mode)
424
-
425
- # Check user/group
426
- if (
427
- (user and remote_file['user'] != user)
428
- or (group and remote_file['group'] != group)
429
- ):
430
- yield chown(remote_filename, user, group)
431
-
432
-
433
- @operation
434
- def template(
435
- state, host, template_filename, remote_filename,
436
- user=None, group=None, mode=None, create_remote_dir=False,
437
- **data
438
- ):
439
- '''
440
- Generate a template and write it to the remote system.
441
-
442
- + template_filename: local template filename
443
- + remote_filename: remote filename
444
- + user: user to own the files
445
- + group: group to own the files
446
- + mode: permissions of the files
447
- + create_remote_dir: create the remote directory if it doesn't exist
448
-
449
- ``create_remote_dir``:
450
- If the remote directory does not exist it will be created using the same
451
- user & group as passed to ``files.put``. The mode will *not* be copied over,
452
- if this is required call ``files.directory`` separately.
453
- '''
454
-
455
- if state.deploy_dir:
456
- template_filename = path.join(state.deploy_dir, template_filename)
457
-
458
- # Ensure host is always available inside templates
459
- data['host'] = host
460
- data['inventory'] = state.inventory
461
-
462
- # Render and make file-like it's output
463
- try:
464
- output = get_template(template_filename).render(data)
465
- except (TemplateSyntaxError, UndefinedError) as e:
466
- _, _, trace = sys.exc_info()
467
-
468
- # Jump through to the *second last* traceback, which contains the line number
469
- # of the error within the in-memory Template object
470
- while trace.tb_next:
471
- if trace.tb_next.tb_next:
472
- trace = trace.tb_next
473
- else: # pragma: no cover
474
- break
475
-
476
- line_number = trace.tb_frame.f_lineno
477
-
478
- # Quickly read the line in question and one above/below for nicer debugging
479
- with open(template_filename, 'r') as f:
480
- template_lines = f.readlines()
481
-
482
- template_lines = [line.strip() for line in template_lines]
483
- relevant_lines = template_lines[max(line_number - 2, 0):line_number + 1]
484
-
485
- raise OperationError('Error in template: {0} (L{1}): {2}\n...\n{3}\n...'.format(
486
- template_filename, line_number, e, '\n'.join(relevant_lines),
487
- ))
488
-
489
- output_file = StringIO(output)
490
- # Set the template attribute for nicer debugging
491
- output_file.template = template_filename
492
-
493
- # Pass to the put function
494
- yield put(
495
- state, host,
496
- output_file, remote_filename,
497
- user=user, group=group, mode=mode,
498
- add_deploy_dir=False,
499
- create_remote_dir=create_remote_dir,
500
- )
501
-
502
-
503
- @operation(pipeline_facts={
504
- 'link': 'name',
505
- })
506
- def link(
507
- state, host, name,
508
- target=None, present=True, assume_present=False,
509
- user=None, group=None, symbolic=True,
510
- create_remote_dir=False,
511
- ):
512
- '''
513
- Add/remove/update links.
514
-
515
- + name: the name of the link
516
- + target: the file/directory the link points to
517
- + present: whether the link should exist
518
- + assume_present: whether to assume the link exists
519
- + user: user to own the link
520
- + group: group to own the link
521
- + symbolic: whether to make a symbolic link (vs hard link)
522
- + create_remote_dir: create the remote directory if it doesn't exist
523
-
524
- ``create_remote_dir``:
525
- If the remote directory does not exist it will be created using the same
526
- user & group as passed to ``files.put``. The mode will *not* be copied over,
527
- if this is required call ``files.directory`` separately.
528
-
529
- Source changes:
530
- If the link exists and points to a different target, pyinfra will remove it and
531
- recreate a new one pointing to then new target.
532
- '''
533
-
534
- if present and not target:
535
- raise OperationError('If present is True target must be provided')
536
-
537
- info = host.fact.link(name)
538
-
539
- # Not a link?
540
- if info is False:
541
- raise OperationError('{0} exists and is not a link'.format(name))
542
-
543
- add_cmd = 'ln{0} {1} {2}'.format(
544
- ' -s' if symbolic else '',
545
- target, name,
546
- )
547
-
548
- remove_cmd = 'rm -f {0}'.format(name)
549
-
550
- # No link and we want it
551
- if not assume_present and info is None and present:
552
- if create_remote_dir:
553
- yield _create_remote_dir(state, host, name, user, group)
554
-
555
- yield add_cmd
556
- if user or group:
557
- yield chown(name, user, group, dereference=False)
558
-
559
- # It exists and we don't want it
560
- elif (assume_present or info) and not present:
561
- yield remove_cmd
562
-
563
- # Exists and want to ensure it's state
564
- elif (assume_present or info) and present:
565
- # If we have an absolute name - prepend to any non-absolute values from the fact
566
- # and/or the soruce.
567
- if path.isabs(name):
568
- link_dirname = path.dirname(name)
569
-
570
- if not path.isabs(target):
571
- target = path.normpath('/'.join((link_dirname, target)))
572
-
573
- if info and not path.isabs(info['link_target']):
574
- info['link_target'] = path.normpath(
575
- '/'.join((link_dirname, info['link_target'])),
576
- )
577
-
578
- # If the target is wrong, remove & recreate the link
579
- if not info or info['link_target'] != target:
580
- yield remove_cmd
581
- yield add_cmd
582
-
583
- # Check user/group
584
- if (
585
- (not info and (user or group))
586
- or (user and info['user'] != user)
587
- or (group and info['group'] != group)
588
- ):
589
- yield chown(name, user, group, dereference=False)
590
-
591
-
592
- @operation(pipeline_facts={
593
- 'file': 'name',
594
- })
595
- def file(
596
- state, host, name,
597
- present=True, assume_present=False,
598
- user=None, group=None, mode=None, touch=False,
599
- create_remote_dir=False,
600
- ):
601
- '''
602
- Add/remove/update files.
603
-
604
- + name: name/path of the remote file
605
- + present: whether the file should exist
606
- + assume_present: whether to assume the file exists
607
- + user: user to own the files
608
- + group: group to own the files
609
- + mode: permissions of the files as an integer, eg: 755
610
- + touch: whether to touch the file
611
- + create_remote_dir: create the remote directory if it doesn't exist
612
-
613
- ``create_remote_dir``:
614
- If the remote directory does not exist it will be created using the same
615
- user & group as passed to ``files.put``. The mode will *not* be copied over,
616
- if this is required call ``files.directory`` separately.
617
- '''
618
-
619
- mode = ensure_mode_int(mode)
620
- info = host.fact.file(name)
621
-
622
- # Not a file?!
623
- if info is False:
624
- raise OperationError('{0} exists and is not a file'.format(name))
625
-
626
- # Doesn't exist & we want it
627
- if not assume_present and info is None and present:
628
- if create_remote_dir:
629
- yield _create_remote_dir(state, host, name, user, group)
630
-
631
- yield 'touch {0}'.format(name)
632
-
633
- if mode:
634
- yield chmod(name, mode)
635
- if user or group:
636
- yield chown(name, user, group)
637
-
638
- # It exists and we don't want it
639
- elif (assume_present or info) and not present:
640
- yield 'rm -f {0}'.format(name)
641
-
642
- # It exists & we want to ensure its state
643
- elif (assume_present or info) and present:
644
- if touch:
645
- yield 'touch {0}'.format(name)
646
-
647
- # Check mode
648
- if mode and (not info or info['mode'] != mode):
649
- yield chmod(name, mode)
650
-
651
- # Check user/group
652
- if (
653
- (not info and (user or group))
654
- or (user and info['user'] != user)
655
- or (group and info['group'] != group)
656
- ):
657
- yield chown(name, user, group)
658
-
659
-
660
- @operation(pipeline_facts={
661
- 'directory': 'name',
662
- })
663
- def directory(
664
- state, host, name,
665
- present=True, assume_present=False,
666
- user=None, group=None, mode=None, recursive=False,
667
- ):
668
- '''
669
- Add/remove/update directories.
670
-
671
- + name: name/patr of the remote folder
672
- + present: whether the folder should exist
673
- + assume_present: whether to assume the directory exists
674
- + user: user to own the folder
675
- + group: group to own the folder
676
- + mode: permissions of the folder
677
- + recursive: recursively apply user/group/mode
678
- '''
679
-
680
- mode = ensure_mode_int(mode)
681
- info = host.fact.directory(name)
682
-
683
- # Not a directory?!
684
- if info is False:
685
- raise OperationError('{0} exists and is not a directory'.format(name))
686
-
687
- # Doesn't exist & we want it
688
- if not assume_present and info is None and present:
689
- yield 'mkdir -p {0}'.format(name)
690
- if mode:
691
- yield chmod(name, mode, recursive=recursive)
692
- if user or group:
693
- yield chown(name, user, group, recursive=recursive)
694
-
695
- # It exists and we don't want it
696
- elif (assume_present or info) and not present:
697
- yield 'rm -rf {0}'.format(name)
698
-
699
- # It exists & we want to ensure its state
700
- elif (assume_present or info) and present:
701
- # Check mode
702
- if mode and (not info or info['mode'] != mode):
703
- yield chmod(name, mode, recursive=recursive)
704
-
705
- # Check user/group
706
- if (
707
- (not info and (user or group))
708
- or (user and info['user'] != user)
709
- or (group and info['group'] != group)
710
- ):
711
- yield chown(name, user, group, recursive=recursive)