pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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 (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -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 +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
@@ -0,0 +1,610 @@
1
+ """
2
+ Manage MySQL databases, users and privileges.
3
+
4
+ Requires the ``mysql`` CLI executable on the target host(s).
5
+
6
+ All operations in this module take four optional arguments:
7
+ + ``mysql_user``: the username to connect to mysql to
8
+ + ``mysql_password``: the password for the connecting user
9
+ + ``mysql_host``: the hostname of the server to connect to
10
+ + ``mysql_port``: the port of the server to connect to
11
+
12
+ See the example/mysql.py
13
+
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pyinfra import host
19
+ from pyinfra.api import MaskString, OperationError, QuoteString, StringCommand, operation
20
+ from pyinfra.facts.mysql import (
21
+ MysqlDatabases,
22
+ MysqlUserGrants,
23
+ MysqlUsers,
24
+ make_execute_mysql_command,
25
+ make_mysql_command,
26
+ )
27
+
28
+
29
+ @operation(is_idempotent=False)
30
+ def sql(
31
+ sql: str,
32
+ database: str | None = None,
33
+ # Details for speaking to MySQL via `mysql` CLI
34
+ mysql_user: str | None = None,
35
+ mysql_password: str | None = None,
36
+ mysql_host: str | None = None,
37
+ mysql_port: int | None = None,
38
+ ):
39
+ """
40
+ Execute arbitrary SQL against MySQL.
41
+
42
+ + sql: SQL command(s) to execute
43
+ + database: optional database to open the connection with
44
+ + mysql_*: global module arguments, see above
45
+ """
46
+
47
+ yield make_execute_mysql_command(
48
+ sql,
49
+ database=database,
50
+ user=mysql_user,
51
+ password=mysql_password,
52
+ host=mysql_host,
53
+ port=mysql_port,
54
+ )
55
+
56
+
57
+ @operation()
58
+ def user(
59
+ user: str,
60
+ present: bool = True,
61
+ user_hostname: str = "localhost",
62
+ password: str | None = None,
63
+ privileges: str | list[str] | None = None,
64
+ # MySQL REQUIRE SSL/TLS options
65
+ require: str | None = None, # SSL or X509
66
+ require_cipher: str | None = None,
67
+ require_issuer: str | None = None,
68
+ require_subject: str | None = None,
69
+ # MySQL WITH resource limit options
70
+ max_connections: int | None = None,
71
+ max_queries_per_hour: int | None = None,
72
+ max_updates_per_hour: int | None = None,
73
+ max_connections_per_hour: int | None = None,
74
+ # Details for speaking to MySQL via `mysql` CLI via `mysql` CLI
75
+ mysql_user: str | None = None,
76
+ mysql_password: str | None = None,
77
+ mysql_host: str | None = None,
78
+ mysql_port: int | None = None,
79
+ ):
80
+ ...
81
+ """
82
+ Add/remove/update MySQL users.
83
+
84
+ + user: the name of the user
85
+ + present: whether the user should exist or not
86
+ + user_hostname: the hostname of the user
87
+ + password: the password of the user (if created)
88
+ + privileges: the global privileges for this user
89
+ + mysql_*: global module arguments, see above
90
+
91
+ Hostname:
92
+ this + ``name`` makes the username - so changing this will create a new
93
+ user, rather than update users with the same ``name``.
94
+
95
+ Password:
96
+ will only be applied if the user does not exist - ie pyinfra cannot
97
+ detect if the current password doesn't match the one provided, so won't
98
+ attempt to change it.
99
+
100
+ **Example:**
101
+
102
+ .. code:: python
103
+
104
+ from pyinfra.operations import mysql
105
+ mysql.user(
106
+ name="Create the pyinfra@localhost MySQL user",
107
+ user="pyinfra",
108
+ password="somepassword",
109
+ )
110
+
111
+ # Create a user with resource limits
112
+ mysql.user(
113
+ name="Create the pyinfra@localhost MySQL user",
114
+ user="pyinfra",
115
+ max_connections=50,
116
+ max_updates_per_hour=10,
117
+ )
118
+
119
+ # Create a user that requires SSL for connections
120
+ mysql.user(
121
+ name="Create the pyinfra@localhost MySQL user",
122
+ user="pyinfra",
123
+ password="somepassword",
124
+ require="SSL",
125
+ )
126
+
127
+ # Create a user that requires a specific certificate
128
+ mysql.user(
129
+ name="Create the pyinfra@localhost MySQL user",
130
+ user="pyinfra",
131
+ password="somepassword",
132
+ require="X509",
133
+ require_issuer="/C=SE/ST=Stockholm...",
134
+ require_cipher="EDH-RSA-DES-CBC3-SHA",
135
+ )
136
+ """
137
+
138
+ if require and require not in ("SSL", "X509"):
139
+ raise OperationError('Invalid `require` value, must be: "SSL" or "X509"')
140
+
141
+ if require != "X509":
142
+ if require_cipher:
143
+ raise OperationError('Cannot set `require_cipher` if `require` is not "X509"')
144
+ if require_issuer:
145
+ raise OperationError('Cannot set `require_issuer` if `require` is not "X509"')
146
+ if require_subject:
147
+ raise OperationError('Cannot set `require_subject` if `require` is not "X509"')
148
+
149
+ current_users = host.get_fact(
150
+ MysqlUsers,
151
+ mysql_user=mysql_user,
152
+ mysql_password=mysql_password,
153
+ mysql_host=mysql_host,
154
+ mysql_port=mysql_port,
155
+ )
156
+
157
+ user_host = "{0}@{1}".format(user, user_hostname)
158
+ is_present = user_host in current_users
159
+
160
+ if not present:
161
+ if is_present:
162
+ yield make_execute_mysql_command(
163
+ 'DROP USER "{0}"@"{1}"'.format(user, user_hostname),
164
+ user=mysql_user,
165
+ password=mysql_password,
166
+ host=mysql_host,
167
+ port=mysql_port,
168
+ )
169
+ else:
170
+ host.noop("mysql user {0}@{1} does not exist".format(user, user_hostname))
171
+ return
172
+
173
+ if present and not is_present:
174
+ sql_bits = ['CREATE USER "{0}"@"{1}"'.format(user, user_hostname)]
175
+ if password:
176
+ sql_bits.append(MaskString('IDENTIFIED BY "{0}"'.format(password)))
177
+
178
+ if require == "SSL":
179
+ sql_bits.append("REQUIRE SSL")
180
+
181
+ if require == "X509":
182
+ sql_bits.append("REQUIRE")
183
+ require_bits = []
184
+
185
+ if require_cipher:
186
+ require_bits.append('CIPHER "{0}"'.format(require_cipher))
187
+ if require_issuer:
188
+ require_bits.append('ISSUER "{0}"'.format(require_issuer))
189
+ if require_subject:
190
+ require_bits.append('SUBJECT "{0}"'.format(require_subject))
191
+
192
+ if not require_bits:
193
+ require_bits.append("X509")
194
+
195
+ sql_bits.extend(require_bits)
196
+
197
+ resource_bits = []
198
+ if max_connections:
199
+ resource_bits.append("MAX_USER_CONNECTIONS {0}".format(max_connections))
200
+ if max_queries_per_hour:
201
+ resource_bits.append("MAX_QUERIES_PER_HOUR {0}".format(max_queries_per_hour))
202
+ if max_updates_per_hour:
203
+ resource_bits.append("MAX_UPDATES_PER_HOUR {0}".format(max_updates_per_hour))
204
+ if max_connections_per_hour:
205
+ resource_bits.append("MAX_CONNECTIONS_PER_HOUR {0}".format(max_connections_per_hour))
206
+
207
+ if resource_bits:
208
+ sql_bits.append("WITH")
209
+ sql_bits.append(" ".join(resource_bits))
210
+
211
+ yield make_execute_mysql_command(
212
+ StringCommand(*sql_bits),
213
+ user=mysql_user,
214
+ password=mysql_password,
215
+ host=mysql_host,
216
+ port=mysql_port,
217
+ )
218
+
219
+ if present and is_present:
220
+ current_user = current_users.get(user_host)
221
+
222
+ alter_bits = []
223
+
224
+ if require == "SSL":
225
+ if current_user["ssl_type"] != "ANY":
226
+ alter_bits.append("REQUIRE SSL")
227
+
228
+ if require == "X509":
229
+ require_bits = []
230
+
231
+ if require_cipher and current_user["ssl_cipher"] != require_cipher:
232
+ require_bits.append('CIPHER "{0}"'.format(require_cipher))
233
+ if require_issuer and current_user["x509_issuer"] != require_issuer:
234
+ require_bits.append('ISSUER "{0}"'.format(require_issuer))
235
+ if require_subject and current_user["x509_subject"] != require_subject:
236
+ require_bits.append('SUBJECT "{0}"'.format(require_subject))
237
+
238
+ if not require_bits:
239
+ if current_user["ssl_type"] != "X509":
240
+ require_bits.append("X509")
241
+
242
+ if require_bits:
243
+ alter_bits.append("REQUIRE")
244
+ alter_bits.extend(require_bits)
245
+
246
+ resource_bits = []
247
+ if max_connections and current_user["max_user_connections"] != max_connections:
248
+ resource_bits.append("MAX_USER_CONNECTIONS {0}".format(max_connections))
249
+ if max_queries_per_hour and current_user["max_questions"] != max_queries_per_hour:
250
+ resource_bits.append("MAX_QUERIES_PER_HOUR {0}".format(max_queries_per_hour))
251
+ if max_updates_per_hour and current_user["max_updates"] != max_updates_per_hour:
252
+ resource_bits.append("MAX_UPDATES_PER_HOUR {0}".format(max_updates_per_hour))
253
+ if max_connections_per_hour and current_user["max_connections"] != max_connections_per_hour:
254
+ resource_bits.append("MAX_CONNECTIONS_PER_HOUR {0}".format(max_connections_per_hour))
255
+
256
+ if resource_bits:
257
+ alter_bits.append("WITH")
258
+ alter_bits.append(" ".join(resource_bits))
259
+
260
+ if alter_bits:
261
+ sql_bits = ['ALTER USER "{0}"@"{1}"'.format(user, user_hostname)]
262
+ sql_bits.extend(alter_bits)
263
+ yield make_execute_mysql_command(
264
+ StringCommand(*sql_bits),
265
+ user=mysql_user,
266
+ password=mysql_password,
267
+ host=mysql_host,
268
+ port=mysql_port,
269
+ )
270
+ else:
271
+ host.noop("mysql user {0}@{1} exists".format(user, user_hostname))
272
+
273
+ # If we're here either the user exists or we just created them; either way
274
+ # now we can check any privileges are set.
275
+ if privileges:
276
+ yield from _privileges._inner(
277
+ user,
278
+ privileges,
279
+ user_hostname=user_hostname,
280
+ mysql_user=mysql_user,
281
+ mysql_password=mysql_password,
282
+ mysql_host=mysql_host,
283
+ mysql_port=mysql_port,
284
+ )
285
+
286
+
287
+ @operation()
288
+ def database(
289
+ database: str,
290
+ # Desired database settings
291
+ present: bool = True,
292
+ collate: str | None = None,
293
+ charset: str | None = None,
294
+ user: str | None = None,
295
+ user_hostname: str = "localhost",
296
+ user_privileges: str | list[str] = "ALL",
297
+ # Details for speaking to MySQL via `mysql` CLI
298
+ mysql_user: str | None = None,
299
+ mysql_password: str | None = None,
300
+ mysql_host: str | None = None,
301
+ mysql_port: int | None = None,
302
+ ):
303
+ ...
304
+ """
305
+ Add/remove MySQL databases.
306
+
307
+ + database: the name of the database
308
+ + present: whether the database should exist or not
309
+ + collate: the collate to use when creating the database
310
+ + charset: the charset to use when creating the database
311
+ + user: MySQL user to grant privileges on this database to
312
+ + user_hostname: the hostname of the MySQL user to grant
313
+ + user_privileges: privileges to grant to any specified user
314
+ + mysql_*: global module arguments, see above
315
+
316
+ Collate/charset:
317
+ these will only be applied if the database does not exist - ie pyinfra
318
+ will not attempt to alter the existing databases collate/character sets.
319
+
320
+ **Example:**
321
+
322
+ .. code:: python
323
+
324
+ mysql.database(
325
+ name="Create the pyinfra_stuff database",
326
+ database="pyinfra_stuff",
327
+ user="pyinfra",
328
+ user_privileges=["SELECT", "INSERT"],
329
+ charset="utf8",
330
+ )
331
+ """
332
+
333
+ current_databases = host.get_fact(
334
+ MysqlDatabases,
335
+ mysql_user=mysql_user,
336
+ mysql_password=mysql_password,
337
+ mysql_host=mysql_host,
338
+ mysql_port=mysql_port,
339
+ )
340
+
341
+ is_present = database in current_databases
342
+
343
+ if not present:
344
+ if is_present:
345
+ yield make_execute_mysql_command(
346
+ "DROP DATABASE `{0}`".format(database),
347
+ user=mysql_user,
348
+ password=mysql_password,
349
+ host=mysql_host,
350
+ port=mysql_port,
351
+ )
352
+ else:
353
+ host.noop("mysql database {0} does not exist".format(database))
354
+ return
355
+
356
+ # We want the database but it doesn't exist
357
+ if present and not is_present:
358
+ sql_bits = ["CREATE DATABASE `{0}`".format(database)]
359
+
360
+ if collate:
361
+ sql_bits.append("COLLATE {0}".format(collate))
362
+
363
+ if charset:
364
+ sql_bits.append("CHARSET {0}".format(charset))
365
+
366
+ yield make_execute_mysql_command(
367
+ " ".join(sql_bits),
368
+ user=mysql_user,
369
+ password=mysql_password,
370
+ host=mysql_host,
371
+ port=mysql_port,
372
+ )
373
+ else:
374
+ host.noop("mysql database {0} exists".format(database))
375
+
376
+ # Ensure any user privileges for this database
377
+ if user and user_privileges:
378
+ yield from privileges._inner(
379
+ user,
380
+ user_hostname=user_hostname,
381
+ privileges=user_privileges,
382
+ database=database,
383
+ mysql_user=mysql_user,
384
+ mysql_password=mysql_password,
385
+ mysql_host=mysql_host,
386
+ mysql_port=mysql_port,
387
+ )
388
+
389
+
390
+ @operation()
391
+ def privileges(
392
+ user: str,
393
+ privileges: str | list[str] | set[str],
394
+ user_hostname="localhost",
395
+ database="*",
396
+ table="*",
397
+ flush=True,
398
+ with_grant_option=False,
399
+ # Details for speaking to MySQL via `mysql` CLI
400
+ mysql_user: str | None = None,
401
+ mysql_password: str | None = None,
402
+ mysql_host: str | None = None,
403
+ mysql_port: int | None = None,
404
+ ):
405
+ """
406
+ Add/remove MySQL privileges for a user, either global, database or table specific.
407
+
408
+ + user: name of the user to manage privileges for
409
+ + privileges: list of privileges the user should have (see also: ``with_grant_option`` argument)
410
+ + user_hostname: the hostname of the user
411
+ + database: name of the database to grant privileges to (defaults to all)
412
+ + table: name of the table to grant privileges to (defaults to all)
413
+ + flush: whether to flush (and update) the privileges table after any changes
414
+ + with_grant_option: whether the grant option privilege should be set
415
+ + mysql_*: global module arguments, see above
416
+ """
417
+
418
+ # Ensure we have a list
419
+ if isinstance(privileges, str):
420
+ privileges = {privileges}
421
+
422
+ if isinstance(privileges, list):
423
+ privileges = set(privileges)
424
+
425
+ if with_grant_option:
426
+ privileges.add("GRANT OPTION")
427
+
428
+ if database != "*":
429
+ database = "`{0}`".format(database)
430
+
431
+ if table != "*":
432
+ table = "`{0}`".format(table)
433
+
434
+ # We can't set privileges on *.tablename as MySQL won't allow it
435
+ if database == "*":
436
+ raise OperationError(
437
+ ("Cannot apply MySQL privileges on {0}.{1}, no database provided").format(
438
+ database,
439
+ table,
440
+ ),
441
+ )
442
+
443
+ database_table = "{0}.{1}".format(database, table)
444
+ user_grants = host.get_fact(
445
+ MysqlUserGrants,
446
+ user=user,
447
+ hostname=user_hostname,
448
+ mysql_user=mysql_user,
449
+ mysql_password=mysql_password,
450
+ mysql_host=mysql_host,
451
+ mysql_port=mysql_port,
452
+ )
453
+
454
+ existing_privileges = set()
455
+ if database_table in user_grants:
456
+ existing_privileges = {
457
+ "ALL" if privilege == "ALL PRIVILEGES" else privilege
458
+ for privilege in user_grants[database_table]
459
+ }
460
+ else:
461
+ user_grants[database_table] = set()
462
+
463
+ def handle_privileges(action, target, privileges_to_apply, with_statement=None):
464
+ command = (
465
+ '{action} {privileges} ON {database}.{table} {target} "{user}"@"{user_hostname}"'
466
+ ).format(
467
+ privileges=", ".join(sorted(privileges_to_apply)),
468
+ action=action,
469
+ target=target,
470
+ database=database,
471
+ table=table,
472
+ user=user,
473
+ user_hostname=user_hostname,
474
+ )
475
+
476
+ if with_statement:
477
+ command += " WITH {with_statement}".format(with_statement=with_statement)
478
+
479
+ yield make_execute_mysql_command(
480
+ command,
481
+ user=mysql_user,
482
+ password=mysql_password,
483
+ host=mysql_host,
484
+ port=mysql_port,
485
+ )
486
+
487
+ # Find / revoke any privileges that exist that do not match the desired state
488
+ privileges_to_revoke = existing_privileges.difference(privileges)
489
+ # Find / grant any privileges that we want but do not exist
490
+ privileges_to_grant = privileges - existing_privileges
491
+
492
+ if privileges_to_grant:
493
+ # We will grant something on this table, no need to revoke "USAGE" as it will be overridden
494
+ privileges_to_revoke.discard("USAGE")
495
+
496
+ if privileges_to_revoke:
497
+ if {"ALL", "GRANT OPTION"} == privileges_to_revoke:
498
+ # This specific case is identified as the alternative syntax for revoke (without ON).
499
+ # To avoid this problem, revoke in 2 steps.
500
+ yield from handle_privileges("REVOKE", "FROM", {"GRANT OPTION"})
501
+ yield from handle_privileges("REVOKE", "FROM", {"ALL PRIVILEGES"})
502
+ else:
503
+ yield from handle_privileges("REVOKE", "FROM", privileges_to_revoke)
504
+
505
+ if privileges_to_grant:
506
+ if {"ALL", "GRANT OPTION"} == privileges_to_grant:
507
+ # ALL must be named by itself and cannot be specified along with other privileges
508
+ # The only exception for us is GRANT OPTION but as a WITH clause
509
+ # So handle this case and let the other ones fail
510
+ yield from handle_privileges("GRANT", "TO", {"ALL PRIVILEGES"}, "GRANT OPTION")
511
+ else:
512
+ yield from handle_privileges("GRANT", "TO", privileges_to_grant)
513
+
514
+ if privileges_to_grant or privileges_to_revoke:
515
+ if flush:
516
+ yield make_execute_mysql_command(
517
+ "FLUSH PRIVILEGES",
518
+ user=mysql_user,
519
+ password=mysql_password,
520
+ host=mysql_host,
521
+ port=mysql_port,
522
+ )
523
+ else:
524
+ host.noop("mysql privileges are already correct")
525
+
526
+
527
+ _privileges = privileges # noqa: E305 (for use where kwarg is the same)
528
+
529
+
530
+ @operation(is_idempotent=False)
531
+ def dump(
532
+ dest: str,
533
+ database: str | None = None,
534
+ # Details for speaking to MySQL via `mysql` CLI
535
+ mysql_user: str | None = None,
536
+ mysql_password: str | None = None,
537
+ mysql_host: str | None = None,
538
+ mysql_port: int | None = None,
539
+ ):
540
+ """
541
+ Dump a MySQL database into a ``.sql`` file. Requires ``mysqldump``.
542
+
543
+ + dest: name of the file to dump the SQL to
544
+ + database: name of the database to dump
545
+ + mysql_*: global module arguments, see above
546
+
547
+ **Example:**
548
+
549
+ .. code:: python
550
+
551
+ mysql.dump(
552
+ name="Dump the pyinfra_stuff database",
553
+ dest="/tmp/pyinfra_stuff.dump",
554
+ database="pyinfra_stuff",
555
+ )
556
+ """
557
+
558
+ yield StringCommand(
559
+ make_mysql_command(
560
+ executable="mysqldump",
561
+ database=database,
562
+ user=mysql_user,
563
+ password=mysql_password,
564
+ host=mysql_host,
565
+ port=mysql_port,
566
+ ),
567
+ ">",
568
+ QuoteString(dest),
569
+ )
570
+
571
+
572
+ @operation(is_idempotent=False)
573
+ def load(
574
+ src: str,
575
+ database: str | None = None,
576
+ # Details for speaking to MySQL via `mysql` CLI
577
+ mysql_user: str | None = None,
578
+ mysql_password: str | None = None,
579
+ mysql_host: str | None = None,
580
+ mysql_port: int | None = None,
581
+ ):
582
+ """
583
+ Load ``.sql`` file into a database.
584
+
585
+ + src: the filename to read from
586
+ + database: name of the database to import into
587
+ + mysql_*: global module arguments, see above
588
+
589
+ **Example:**
590
+
591
+ .. code:: python
592
+
593
+ mysql.load(
594
+ name="Import the pyinfra_stuff dump into pyinfra_stuff_copy",
595
+ src="/tmp/pyinfra_stuff.dump",
596
+ database="pyinfra_stuff_copy",
597
+ )
598
+ """
599
+
600
+ yield StringCommand(
601
+ make_mysql_command(
602
+ database=database,
603
+ user=mysql_user,
604
+ password=mysql_password,
605
+ host=mysql_host,
606
+ port=mysql_port,
607
+ ),
608
+ "<",
609
+ QuoteString(src),
610
+ )
@@ -0,0 +1,57 @@
1
+ """
2
+ Manage npm (aka node aka Node.js) packages.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyinfra import host
8
+ from pyinfra.api import operation
9
+ from pyinfra.facts.npm import NpmPackages
10
+
11
+ from .util.packaging import ensure_packages
12
+
13
+
14
+ @operation()
15
+ def packages(
16
+ packages: str | list[str] | None = None,
17
+ present=True,
18
+ latest=False,
19
+ directory: str | None = None,
20
+ ):
21
+ """
22
+ Install/remove/update npm packages.
23
+
24
+ + packages: list of packages to ensure
25
+ + present: whether the packages should be present
26
+ + latest: whether to upgrade packages without a specified version
27
+ + directory: directory to manage packages for, defaults to global
28
+
29
+ Versions:
30
+ Package versions can be pinned like npm: ``<pkg>@<version>``.
31
+ """
32
+
33
+ current_packages = host.get_fact(NpmPackages, directory=directory)
34
+
35
+ install_command = (
36
+ "npm install -g" if directory is None else "cd {0} && npm install".format(directory)
37
+ )
38
+
39
+ uninstall_command = (
40
+ "npm uninstall -g" if directory is None else "cd {0} && npm uninstall".format(directory)
41
+ )
42
+
43
+ upgrade_command = (
44
+ "npm update -g" if directory is None else "cd {0} && npm update".format(directory)
45
+ )
46
+
47
+ yield from ensure_packages(
48
+ host,
49
+ packages,
50
+ current_packages,
51
+ present,
52
+ install_command=install_command,
53
+ uninstall_command=uninstall_command,
54
+ upgrade_command=upgrade_command,
55
+ version_join="@",
56
+ latest=latest,
57
+ )