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
@@ -0,0 +1,609 @@
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
+ mysql.user(
105
+ name="Create the pyinfra@localhost MySQL user",
106
+ user="pyinfra",
107
+ password="somepassword",
108
+ )
109
+
110
+ # Create a user with resource limits
111
+ mysql.user(
112
+ name="Create the pyinfra@localhost MySQL user",
113
+ user="pyinfra",
114
+ max_connections=50,
115
+ max_updates_per_hour=10,
116
+ )
117
+
118
+ # Create a user that requires SSL for connections
119
+ mysql.user(
120
+ name="Create the pyinfra@localhost MySQL user",
121
+ user="pyinfra",
122
+ password="somepassword",
123
+ require="SSL",
124
+ )
125
+
126
+ # Create a user that requires a specific certificate
127
+ mysql.user(
128
+ name="Create the pyinfra@localhost MySQL user",
129
+ user="pyinfra",
130
+ password="somepassword",
131
+ require="X509",
132
+ require_issuer="/C=SE/ST=Stockholm...",
133
+ require_cipher="EDH-RSA-DES-CBC3-SHA",
134
+ )
135
+ """
136
+
137
+ if require and require not in ("SSL", "X509"):
138
+ raise OperationError('Invalid `require` value, must be: "SSL" or "X509"')
139
+
140
+ if require != "X509":
141
+ if require_cipher:
142
+ raise OperationError('Cannot set `require_cipher` if `require` is not "X509"')
143
+ if require_issuer:
144
+ raise OperationError('Cannot set `require_issuer` if `require` is not "X509"')
145
+ if require_subject:
146
+ raise OperationError('Cannot set `require_subject` if `require` is not "X509"')
147
+
148
+ current_users = host.get_fact(
149
+ MysqlUsers,
150
+ mysql_user=mysql_user,
151
+ mysql_password=mysql_password,
152
+ mysql_host=mysql_host,
153
+ mysql_port=mysql_port,
154
+ )
155
+
156
+ user_host = "{0}@{1}".format(user, user_hostname)
157
+ is_present = user_host in current_users
158
+
159
+ if not present:
160
+ if is_present:
161
+ yield make_execute_mysql_command(
162
+ 'DROP USER "{0}"@"{1}"'.format(user, user_hostname),
163
+ user=mysql_user,
164
+ password=mysql_password,
165
+ host=mysql_host,
166
+ port=mysql_port,
167
+ )
168
+ else:
169
+ host.noop("mysql user {0}@{1} does not exist".format(user, user_hostname))
170
+ return
171
+
172
+ if present and not is_present:
173
+ sql_bits = ['CREATE USER "{0}"@"{1}"'.format(user, user_hostname)]
174
+ if password:
175
+ sql_bits.append(MaskString('IDENTIFIED BY "{0}"'.format(password)))
176
+
177
+ if require == "SSL":
178
+ sql_bits.append("REQUIRE SSL")
179
+
180
+ if require == "X509":
181
+ sql_bits.append("REQUIRE")
182
+ require_bits = []
183
+
184
+ if require_cipher:
185
+ require_bits.append('CIPHER "{0}"'.format(require_cipher))
186
+ if require_issuer:
187
+ require_bits.append('ISSUER "{0}"'.format(require_issuer))
188
+ if require_subject:
189
+ require_bits.append('SUBJECT "{0}"'.format(require_subject))
190
+
191
+ if not require_bits:
192
+ require_bits.append("X509")
193
+
194
+ sql_bits.extend(require_bits)
195
+
196
+ resource_bits = []
197
+ if max_connections:
198
+ resource_bits.append("MAX_USER_CONNECTIONS {0}".format(max_connections))
199
+ if max_queries_per_hour:
200
+ resource_bits.append("MAX_QUERIES_PER_HOUR {0}".format(max_queries_per_hour))
201
+ if max_updates_per_hour:
202
+ resource_bits.append("MAX_UPDATES_PER_HOUR {0}".format(max_updates_per_hour))
203
+ if max_connections_per_hour:
204
+ resource_bits.append("MAX_CONNECTIONS_PER_HOUR {0}".format(max_connections_per_hour))
205
+
206
+ if resource_bits:
207
+ sql_bits.append("WITH")
208
+ sql_bits.append(" ".join(resource_bits))
209
+
210
+ yield make_execute_mysql_command(
211
+ StringCommand(*sql_bits),
212
+ user=mysql_user,
213
+ password=mysql_password,
214
+ host=mysql_host,
215
+ port=mysql_port,
216
+ )
217
+
218
+ if present and is_present:
219
+ current_user = current_users.get(user_host)
220
+
221
+ alter_bits = []
222
+
223
+ if require == "SSL":
224
+ if current_user["ssl_type"] != "ANY":
225
+ alter_bits.append("REQUIRE SSL")
226
+
227
+ if require == "X509":
228
+ require_bits = []
229
+
230
+ if require_cipher and current_user["ssl_cipher"] != require_cipher:
231
+ require_bits.append('CIPHER "{0}"'.format(require_cipher))
232
+ if require_issuer and current_user["x509_issuer"] != require_issuer:
233
+ require_bits.append('ISSUER "{0}"'.format(require_issuer))
234
+ if require_subject and current_user["x509_subject"] != require_subject:
235
+ require_bits.append('SUBJECT "{0}"'.format(require_subject))
236
+
237
+ if not require_bits:
238
+ if current_user["ssl_type"] != "X509":
239
+ require_bits.append("X509")
240
+
241
+ if require_bits:
242
+ alter_bits.append("REQUIRE")
243
+ alter_bits.extend(require_bits)
244
+
245
+ resource_bits = []
246
+ if max_connections and current_user["max_user_connections"] != max_connections:
247
+ resource_bits.append("MAX_USER_CONNECTIONS {0}".format(max_connections))
248
+ if max_queries_per_hour and current_user["max_questions"] != max_queries_per_hour:
249
+ resource_bits.append("MAX_QUERIES_PER_HOUR {0}".format(max_queries_per_hour))
250
+ if max_updates_per_hour and current_user["max_updates"] != max_updates_per_hour:
251
+ resource_bits.append("MAX_UPDATES_PER_HOUR {0}".format(max_updates_per_hour))
252
+ if max_connections_per_hour and current_user["max_connections"] != max_connections_per_hour:
253
+ resource_bits.append("MAX_CONNECTIONS_PER_HOUR {0}".format(max_connections_per_hour))
254
+
255
+ if resource_bits:
256
+ alter_bits.append("WITH")
257
+ alter_bits.append(" ".join(resource_bits))
258
+
259
+ if alter_bits:
260
+ sql_bits = ['ALTER USER "{0}"@"{1}"'.format(user, user_hostname)]
261
+ sql_bits.extend(alter_bits)
262
+ yield make_execute_mysql_command(
263
+ StringCommand(*sql_bits),
264
+ user=mysql_user,
265
+ password=mysql_password,
266
+ host=mysql_host,
267
+ port=mysql_port,
268
+ )
269
+ else:
270
+ host.noop("mysql user {0}@{1} exists".format(user, user_hostname))
271
+
272
+ # If we're here either the user exists or we just created them; either way
273
+ # now we can check any privileges are set.
274
+ if privileges:
275
+ yield from _privileges._inner(
276
+ user,
277
+ privileges,
278
+ user_hostname=user_hostname,
279
+ mysql_user=mysql_user,
280
+ mysql_password=mysql_password,
281
+ mysql_host=mysql_host,
282
+ mysql_port=mysql_port,
283
+ )
284
+
285
+
286
+ @operation()
287
+ def database(
288
+ database: str,
289
+ # Desired database settings
290
+ present: bool = True,
291
+ collate: str | None = None,
292
+ charset: str | None = None,
293
+ user: str | None = None,
294
+ user_hostname: str = "localhost",
295
+ user_privileges: str | list[str] = "ALL",
296
+ # Details for speaking to MySQL via `mysql` CLI
297
+ mysql_user: str | None = None,
298
+ mysql_password: str | None = None,
299
+ mysql_host: str | None = None,
300
+ mysql_port: int | None = None,
301
+ ):
302
+ ...
303
+ """
304
+ Add/remove MySQL databases.
305
+
306
+ + database: the name of the database
307
+ + present: whether the database should exist or not
308
+ + collate: the collate to use when creating the database
309
+ + charset: the charset to use when creating the database
310
+ + user: MySQL user to grant privileges on this database to
311
+ + user_hostname: the hostname of the MySQL user to grant
312
+ + user_privileges: privileges to grant to any specified user
313
+ + mysql_*: global module arguments, see above
314
+
315
+ Collate/charset:
316
+ these will only be applied if the database does not exist - ie pyinfra
317
+ will not attempt to alter the existing databases collate/character sets.
318
+
319
+ **Example:**
320
+
321
+ .. code:: python
322
+
323
+ mysql.database(
324
+ name="Create the pyinfra_stuff database",
325
+ database="pyinfra_stuff",
326
+ user="pyinfra",
327
+ user_privileges=["SELECT", "INSERT"],
328
+ charset="utf8",
329
+ )
330
+ """
331
+
332
+ current_databases = host.get_fact(
333
+ MysqlDatabases,
334
+ mysql_user=mysql_user,
335
+ mysql_password=mysql_password,
336
+ mysql_host=mysql_host,
337
+ mysql_port=mysql_port,
338
+ )
339
+
340
+ is_present = database in current_databases
341
+
342
+ if not present:
343
+ if is_present:
344
+ yield make_execute_mysql_command(
345
+ "DROP DATABASE `{0}`".format(database),
346
+ user=mysql_user,
347
+ password=mysql_password,
348
+ host=mysql_host,
349
+ port=mysql_port,
350
+ )
351
+ else:
352
+ host.noop("mysql database {0} does not exist".format(database))
353
+ return
354
+
355
+ # We want the database but it doesn't exist
356
+ if present and not is_present:
357
+ sql_bits = ["CREATE DATABASE `{0}`".format(database)]
358
+
359
+ if collate:
360
+ sql_bits.append("COLLATE {0}".format(collate))
361
+
362
+ if charset:
363
+ sql_bits.append("CHARSET {0}".format(charset))
364
+
365
+ yield make_execute_mysql_command(
366
+ " ".join(sql_bits),
367
+ user=mysql_user,
368
+ password=mysql_password,
369
+ host=mysql_host,
370
+ port=mysql_port,
371
+ )
372
+ else:
373
+ host.noop("mysql database {0} exists".format(database))
374
+
375
+ # Ensure any user privileges for this database
376
+ if user and user_privileges:
377
+ yield from privileges._inner(
378
+ user,
379
+ user_hostname=user_hostname,
380
+ privileges=user_privileges,
381
+ database=database,
382
+ mysql_user=mysql_user,
383
+ mysql_password=mysql_password,
384
+ mysql_host=mysql_host,
385
+ mysql_port=mysql_port,
386
+ )
387
+
388
+
389
+ @operation()
390
+ def privileges(
391
+ user: str,
392
+ privileges: str | list[str] | set[str],
393
+ user_hostname="localhost",
394
+ database="*",
395
+ table="*",
396
+ flush=True,
397
+ with_grant_option=False,
398
+ # Details for speaking to MySQL via `mysql` CLI
399
+ mysql_user: str | None = None,
400
+ mysql_password: str | None = None,
401
+ mysql_host: str | None = None,
402
+ mysql_port: int | None = None,
403
+ ):
404
+ """
405
+ Add/remove MySQL privileges for a user, either global, database or table specific.
406
+
407
+ + user: name of the user to manage privileges for
408
+ + privileges: list of privileges the user should have (see also: ``with_grant_option`` argument)
409
+ + user_hostname: the hostname of the user
410
+ + database: name of the database to grant privileges to (defaults to all)
411
+ + table: name of the table to grant privileges to (defaults to all)
412
+ + flush: whether to flush (and update) the privileges table after any changes
413
+ + with_grant_option: whether the grant option privilege should be set
414
+ + mysql_*: global module arguments, see above
415
+ """
416
+
417
+ # Ensure we have a list
418
+ if isinstance(privileges, str):
419
+ privileges = {privileges}
420
+
421
+ if isinstance(privileges, list):
422
+ privileges = set(privileges)
423
+
424
+ if with_grant_option:
425
+ privileges.add("GRANT OPTION")
426
+
427
+ if database != "*":
428
+ database = "`{0}`".format(database)
429
+
430
+ if table != "*":
431
+ table = "`{0}`".format(table)
432
+
433
+ # We can't set privileges on *.tablename as MySQL won't allow it
434
+ if database == "*":
435
+ raise OperationError(
436
+ ("Cannot apply MySQL privileges on {0}.{1}, no database provided").format(
437
+ database,
438
+ table,
439
+ ),
440
+ )
441
+
442
+ database_table = "{0}.{1}".format(database, table)
443
+ user_grants = host.get_fact(
444
+ MysqlUserGrants,
445
+ user=user,
446
+ hostname=user_hostname,
447
+ mysql_user=mysql_user,
448
+ mysql_password=mysql_password,
449
+ mysql_host=mysql_host,
450
+ mysql_port=mysql_port,
451
+ )
452
+
453
+ existing_privileges = set()
454
+ if database_table in user_grants:
455
+ existing_privileges = {
456
+ "ALL" if privilege == "ALL PRIVILEGES" else privilege
457
+ for privilege in user_grants[database_table]
458
+ }
459
+ else:
460
+ user_grants[database_table] = set()
461
+
462
+ def handle_privileges(action, target, privileges_to_apply, with_statement=None):
463
+ command = (
464
+ '{action} {privileges} ON {database}.{table} {target} "{user}"@"{user_hostname}"'
465
+ ).format(
466
+ privileges=", ".join(sorted(privileges_to_apply)),
467
+ action=action,
468
+ target=target,
469
+ database=database,
470
+ table=table,
471
+ user=user,
472
+ user_hostname=user_hostname,
473
+ )
474
+
475
+ if with_statement:
476
+ command += " WITH {with_statement}".format(with_statement=with_statement)
477
+
478
+ yield make_execute_mysql_command(
479
+ command,
480
+ user=mysql_user,
481
+ password=mysql_password,
482
+ host=mysql_host,
483
+ port=mysql_port,
484
+ )
485
+
486
+ # Find / revoke any privileges that exist that do not match the desired state
487
+ privileges_to_revoke = existing_privileges.difference(privileges)
488
+ # Find / grant any privileges that we want but do not exist
489
+ privileges_to_grant = privileges - existing_privileges
490
+
491
+ if privileges_to_grant:
492
+ # We will grant something on this table, no need to revoke "USAGE" as it will be overridden
493
+ privileges_to_revoke.discard("USAGE")
494
+
495
+ if privileges_to_revoke:
496
+ if {"ALL", "GRANT OPTION"} == privileges_to_revoke:
497
+ # This specific case is identified as the alternative syntax for revoke (without ON).
498
+ # To avoid this problem, revoke in 2 steps.
499
+ yield from handle_privileges("REVOKE", "FROM", {"GRANT OPTION"})
500
+ yield from handle_privileges("REVOKE", "FROM", {"ALL PRIVILEGES"})
501
+ else:
502
+ yield from handle_privileges("REVOKE", "FROM", privileges_to_revoke)
503
+
504
+ if privileges_to_grant:
505
+ if {"ALL", "GRANT OPTION"} == privileges_to_grant:
506
+ # ALL must be named by itself and cannot be specified along with other privileges
507
+ # The only exception for us is GRANT OPTION but as a WITH clause
508
+ # So handle this case and let the other ones fail
509
+ yield from handle_privileges("GRANT", "TO", {"ALL PRIVILEGES"}, "GRANT OPTION")
510
+ else:
511
+ yield from handle_privileges("GRANT", "TO", privileges_to_grant)
512
+
513
+ if privileges_to_grant or privileges_to_revoke:
514
+ if flush:
515
+ yield make_execute_mysql_command(
516
+ "FLUSH PRIVILEGES",
517
+ user=mysql_user,
518
+ password=mysql_password,
519
+ host=mysql_host,
520
+ port=mysql_port,
521
+ )
522
+ else:
523
+ host.noop("mysql privileges are already correct")
524
+
525
+
526
+ _privileges = privileges # noqa: E305 (for use where kwarg is the same)
527
+
528
+
529
+ @operation(is_idempotent=False)
530
+ def dump(
531
+ dest: str,
532
+ database: str | None = None,
533
+ # Details for speaking to MySQL via `mysql` CLI
534
+ mysql_user: str | None = None,
535
+ mysql_password: str | None = None,
536
+ mysql_host: str | None = None,
537
+ mysql_port: int | None = None,
538
+ ):
539
+ """
540
+ Dump a MySQL database into a ``.sql`` file. Requires ``mysqldump``.
541
+
542
+ + dest: name of the file to dump the SQL to
543
+ + database: name of the database to dump
544
+ + mysql_*: global module arguments, see above
545
+
546
+ **Example:**
547
+
548
+ .. code:: python
549
+
550
+ mysql.dump(
551
+ name="Dump the pyinfra_stuff database",
552
+ dest="/tmp/pyinfra_stuff.dump",
553
+ database="pyinfra_stuff",
554
+ )
555
+ """
556
+
557
+ yield StringCommand(
558
+ make_mysql_command(
559
+ executable="mysqldump",
560
+ database=database,
561
+ user=mysql_user,
562
+ password=mysql_password,
563
+ host=mysql_host,
564
+ port=mysql_port,
565
+ ),
566
+ ">",
567
+ QuoteString(dest),
568
+ )
569
+
570
+
571
+ @operation(is_idempotent=False)
572
+ def load(
573
+ src: str,
574
+ database: str | None = None,
575
+ # Details for speaking to MySQL via `mysql` CLI
576
+ mysql_user: str | None = None,
577
+ mysql_password: str | None = None,
578
+ mysql_host: str | None = None,
579
+ mysql_port: int | None = None,
580
+ ):
581
+ """
582
+ Load ``.sql`` file into a database.
583
+
584
+ + src: the filename to read from
585
+ + database: name of the database to import into
586
+ + mysql_*: global module arguments, see above
587
+
588
+ **Example:**
589
+
590
+ .. code:: python
591
+
592
+ mysql.load(
593
+ name="Import the pyinfra_stuff dump into pyinfra_stuff_copy",
594
+ src="/tmp/pyinfra_stuff.dump",
595
+ database="pyinfra_stuff_copy",
596
+ )
597
+ """
598
+
599
+ yield StringCommand(
600
+ make_mysql_command(
601
+ database=database,
602
+ user=mysql_user,
603
+ password=mysql_password,
604
+ host=mysql_host,
605
+ port=mysql_port,
606
+ ),
607
+ "<",
608
+ QuoteString(src),
609
+ )
@@ -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
+ )