pyinfra 3.1.1__py2.py3-none-any.whl → 3.3__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinfra/api/arguments.py +9 -2
- pyinfra/api/arguments_typed.py +4 -5
- pyinfra/api/command.py +22 -3
- pyinfra/api/config.py +5 -2
- pyinfra/api/deploy.py +4 -2
- pyinfra/api/facts.py +3 -0
- pyinfra/api/host.py +15 -7
- pyinfra/api/operation.py +2 -1
- pyinfra/api/state.py +1 -1
- pyinfra/connectors/base.py +34 -8
- pyinfra/connectors/chroot.py +7 -2
- pyinfra/connectors/docker.py +24 -8
- pyinfra/connectors/dockerssh.py +7 -2
- pyinfra/connectors/local.py +7 -2
- pyinfra/connectors/ssh.py +9 -2
- pyinfra/connectors/sshuserclient/client.py +42 -14
- pyinfra/connectors/sshuserclient/config.py +2 -0
- pyinfra/connectors/terraform.py +1 -1
- pyinfra/connectors/util.py +13 -9
- pyinfra/context.py +9 -2
- pyinfra/facts/apk.py +8 -1
- pyinfra/facts/apt.py +68 -0
- pyinfra/facts/brew.py +13 -0
- pyinfra/facts/bsdinit.py +3 -0
- pyinfra/facts/cargo.py +5 -0
- pyinfra/facts/choco.py +6 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +10 -0
- pyinfra/facts/dnf.py +5 -0
- pyinfra/facts/docker.py +16 -0
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +112 -7
- pyinfra/facts/flatpak.py +7 -0
- pyinfra/facts/freebsd.py +75 -0
- pyinfra/facts/gem.py +5 -0
- pyinfra/facts/git.py +12 -2
- pyinfra/facts/gpg.py +7 -0
- pyinfra/facts/hardware.py +13 -0
- pyinfra/facts/iptables.py +9 -1
- pyinfra/facts/launchd.py +5 -0
- pyinfra/facts/lxd.py +5 -0
- pyinfra/facts/mysql.py +9 -2
- pyinfra/facts/npm.py +5 -0
- pyinfra/facts/openrc.py +8 -0
- pyinfra/facts/opkg.py +245 -0
- pyinfra/facts/pacman.py +9 -1
- pyinfra/facts/pip.py +5 -0
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +4 -0
- pyinfra/facts/pkgin.py +5 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +10 -2
- pyinfra/facts/rpm.py +11 -0
- pyinfra/facts/runit.py +7 -0
- pyinfra/facts/selinux.py +16 -0
- pyinfra/facts/server.py +87 -79
- pyinfra/facts/snap.py +7 -0
- pyinfra/facts/systemd.py +5 -0
- pyinfra/facts/sysvinit.py +4 -0
- pyinfra/facts/upstart.py +5 -0
- pyinfra/facts/util/__init__.py +4 -1
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/vzctl.py +5 -0
- pyinfra/facts/xbps.py +6 -1
- pyinfra/facts/yum.py +5 -0
- pyinfra/facts/zfs.py +41 -21
- pyinfra/facts/zypper.py +5 -0
- pyinfra/local.py +3 -2
- pyinfra/operations/apt.py +36 -22
- pyinfra/operations/crontab.py +189 -0
- pyinfra/operations/docker.py +61 -56
- pyinfra/operations/files.py +65 -1
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/git.py +23 -7
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pip.py +3 -2
- pyinfra/operations/pipx.py +90 -0
- pyinfra/operations/postgres.py +114 -27
- pyinfra/operations/runit.py +2 -0
- pyinfra/operations/server.py +9 -181
- pyinfra/operations/util/docker.py +44 -22
- pyinfra/operations/zfs.py +3 -3
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/LICENSE.md +1 -1
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/METADATA +25 -25
- pyinfra-3.3.dist-info/RECORD +187 -0
- pyinfra_cli/exceptions.py +5 -0
- pyinfra_cli/inventory.py +26 -9
- pyinfra_cli/log.py +3 -0
- pyinfra_cli/main.py +9 -8
- pyinfra_cli/prints.py +19 -4
- pyinfra_cli/util.py +3 -0
- pyinfra_cli/virtualenv.py +1 -1
- tests/test_cli/test_cli_deploy.py +15 -13
- tests/test_cli/test_cli_inventory.py +53 -0
- tests/test_connectors/test_ssh.py +302 -182
- tests/test_connectors/test_sshuserclient.py +68 -1
- pyinfra-3.1.1.dist-info/RECORD +0 -172
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/WHEEL +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/top_level.txt +0 -0
pyinfra/operations/postgres.py
CHANGED
|
@@ -8,6 +8,7 @@ All operations in this module take four optional arguments:
|
|
|
8
8
|
+ ``psql_password``: the password for the connecting user
|
|
9
9
|
+ ``psql_host``: the hostname of the server to connect to
|
|
10
10
|
+ ``psql_port``: the port of the server to connect to
|
|
11
|
+
+ ``psql_database``: the database on the server to connect to
|
|
11
12
|
|
|
12
13
|
See example/postgresql.py for detailed example
|
|
13
14
|
|
|
@@ -28,48 +29,48 @@ from pyinfra.facts.postgres import (
|
|
|
28
29
|
@operation(is_idempotent=False)
|
|
29
30
|
def sql(
|
|
30
31
|
sql: str,
|
|
31
|
-
database: str | None = None,
|
|
32
32
|
# Details for speaking to PostgreSQL via `psql` CLI
|
|
33
33
|
psql_user: str | None = None,
|
|
34
34
|
psql_password: str | None = None,
|
|
35
35
|
psql_host: str | None = None,
|
|
36
36
|
psql_port: int | None = None,
|
|
37
|
+
psql_database: str | None = None,
|
|
37
38
|
):
|
|
38
39
|
"""
|
|
39
40
|
Execute arbitrary SQL against PostgreSQL.
|
|
40
41
|
|
|
41
42
|
+ sql: SQL command(s) to execute
|
|
42
|
-
+ database: optional database to execute against
|
|
43
43
|
+ psql_*: global module arguments, see above
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
46
|
yield make_execute_psql_command(
|
|
47
47
|
sql,
|
|
48
|
-
database=database,
|
|
49
48
|
user=psql_user,
|
|
50
49
|
password=psql_password,
|
|
51
50
|
host=psql_host,
|
|
52
51
|
port=psql_port,
|
|
52
|
+
database=psql_database,
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
@operation()
|
|
57
57
|
def role(
|
|
58
58
|
role: str,
|
|
59
|
-
present=True,
|
|
59
|
+
present: bool = True,
|
|
60
60
|
password: str | None = None,
|
|
61
|
-
login=True,
|
|
62
|
-
superuser=False,
|
|
63
|
-
inherit=False,
|
|
64
|
-
createdb=False,
|
|
65
|
-
createrole=False,
|
|
66
|
-
replication=False,
|
|
61
|
+
login: bool = True,
|
|
62
|
+
superuser: bool = False,
|
|
63
|
+
inherit: bool = False,
|
|
64
|
+
createdb: bool = False,
|
|
65
|
+
createrole: bool = False,
|
|
66
|
+
replication: bool = False,
|
|
67
67
|
connection_limit: int | None = None,
|
|
68
68
|
# Details for speaking to PostgreSQL via `psql` CLI
|
|
69
69
|
psql_user: str | None = None,
|
|
70
70
|
psql_password: str | None = None,
|
|
71
71
|
psql_host: str | None = None,
|
|
72
72
|
psql_port: int | None = None,
|
|
73
|
+
psql_database: str | None = None,
|
|
73
74
|
):
|
|
74
75
|
"""
|
|
75
76
|
Add/remove PostgreSQL roles.
|
|
@@ -101,7 +102,7 @@ def role(
|
|
|
101
102
|
password="somepassword",
|
|
102
103
|
superuser=True,
|
|
103
104
|
login=True,
|
|
104
|
-
|
|
105
|
+
_sudo_user="postgres",
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
"""
|
|
@@ -112,6 +113,7 @@ def role(
|
|
|
112
113
|
psql_password=psql_password,
|
|
113
114
|
psql_host=psql_host,
|
|
114
115
|
psql_port=psql_port,
|
|
116
|
+
psql_database=psql_database,
|
|
115
117
|
)
|
|
116
118
|
|
|
117
119
|
is_present = role in roles
|
|
@@ -125,6 +127,7 @@ def role(
|
|
|
125
127
|
password=psql_password,
|
|
126
128
|
host=psql_host,
|
|
127
129
|
port=psql_port,
|
|
130
|
+
database=psql_database,
|
|
128
131
|
)
|
|
129
132
|
else:
|
|
130
133
|
host.noop("postgresql role {0} does not exist".format(role))
|
|
@@ -157,9 +160,50 @@ def role(
|
|
|
157
160
|
password=psql_password,
|
|
158
161
|
host=psql_host,
|
|
159
162
|
port=psql_port,
|
|
163
|
+
database=psql_database,
|
|
160
164
|
)
|
|
161
165
|
else:
|
|
162
|
-
|
|
166
|
+
# Check if any attributes need updating
|
|
167
|
+
current_role = roles[role]
|
|
168
|
+
should_execute = False
|
|
169
|
+
sql_bits = ['ALTER ROLE "{0}"'.format(role)]
|
|
170
|
+
if login and "login" in current_role and current_role["login"] != login:
|
|
171
|
+
sql_bits.append("LOGIN")
|
|
172
|
+
should_execute = True
|
|
173
|
+
if superuser and "superuser" in current_role and current_role["superuser"] != superuser:
|
|
174
|
+
sql_bits.append("SUPERUSER")
|
|
175
|
+
should_execute = True
|
|
176
|
+
if inherit and "inherit" in current_role and current_role["inherit"] != inherit:
|
|
177
|
+
sql_bits.append("INHERIT")
|
|
178
|
+
should_execute = True
|
|
179
|
+
if createdb and "createdb" in current_role and current_role["createdb"] != createdb:
|
|
180
|
+
sql_bits.append("CREATEDB")
|
|
181
|
+
should_execute = True
|
|
182
|
+
if createrole and "createrole" in current_role and current_role["createrole"] != createrole:
|
|
183
|
+
sql_bits.append("CREATEROLE")
|
|
184
|
+
should_execute = True
|
|
185
|
+
if (
|
|
186
|
+
connection_limit
|
|
187
|
+
and "connection_limit" in current_role
|
|
188
|
+
and roles[role]["connection_limit"] != connection_limit
|
|
189
|
+
):
|
|
190
|
+
sql_bits.append("CONNECTION LIMIT {0}".format(connection_limit))
|
|
191
|
+
should_execute = True
|
|
192
|
+
if password:
|
|
193
|
+
sql_bits.append(MaskString("PASSWORD '{0}'".format(password)))
|
|
194
|
+
should_execute = True
|
|
195
|
+
|
|
196
|
+
if should_execute:
|
|
197
|
+
yield make_execute_psql_command(
|
|
198
|
+
StringCommand(*sql_bits),
|
|
199
|
+
user=psql_user,
|
|
200
|
+
password=psql_password,
|
|
201
|
+
host=psql_host,
|
|
202
|
+
port=psql_port,
|
|
203
|
+
database=psql_database,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
host.noop("postgresql role {0} exists and does not need updates".format(role))
|
|
163
207
|
|
|
164
208
|
|
|
165
209
|
@operation()
|
|
@@ -178,6 +222,7 @@ def database(
|
|
|
178
222
|
psql_password: str | None = None,
|
|
179
223
|
psql_host: str | None = None,
|
|
180
224
|
psql_port: int | None = None,
|
|
225
|
+
psql_database: str | None = None,
|
|
181
226
|
):
|
|
182
227
|
"""
|
|
183
228
|
Add/remove PostgreSQL databases.
|
|
@@ -194,9 +239,8 @@ def database(
|
|
|
194
239
|
+ psql_*: global module arguments, see above
|
|
195
240
|
|
|
196
241
|
Updates:
|
|
197
|
-
pyinfra will
|
|
198
|
-
|
|
199
|
-
operation will make no changes).
|
|
242
|
+
pyinfra will change existing databases - but some parameters are not
|
|
243
|
+
changeable (template, encoding, lc_collate and lc_ctype).
|
|
200
244
|
|
|
201
245
|
**Example:**
|
|
202
246
|
|
|
@@ -207,7 +251,7 @@ def database(
|
|
|
207
251
|
database="pyinfra_stuff",
|
|
208
252
|
owner="pyinfra",
|
|
209
253
|
encoding="UTF8",
|
|
210
|
-
|
|
254
|
+
_sudo_user="postgres",
|
|
211
255
|
)
|
|
212
256
|
|
|
213
257
|
"""
|
|
@@ -218,6 +262,7 @@ def database(
|
|
|
218
262
|
psql_password=psql_password,
|
|
219
263
|
psql_host=psql_host,
|
|
220
264
|
psql_port=psql_port,
|
|
265
|
+
psql_database=psql_database,
|
|
221
266
|
)
|
|
222
267
|
|
|
223
268
|
is_present = database in current_databases
|
|
@@ -230,6 +275,7 @@ def database(
|
|
|
230
275
|
password=psql_password,
|
|
231
276
|
host=psql_host,
|
|
232
277
|
port=psql_port,
|
|
278
|
+
database=psql_database,
|
|
233
279
|
)
|
|
234
280
|
else:
|
|
235
281
|
host.noop("postgresql database {0} does not exist".format(database))
|
|
@@ -243,8 +289,8 @@ def database(
|
|
|
243
289
|
("OWNER", '"{0}"'.format(owner) if owner else owner),
|
|
244
290
|
("TEMPLATE", template),
|
|
245
291
|
("ENCODING", encoding),
|
|
246
|
-
("LC_COLLATE", lc_collate),
|
|
247
|
-
("LC_CTYPE", lc_ctype),
|
|
292
|
+
("LC_COLLATE", "'{0}'".format(lc_collate) if lc_collate else lc_collate),
|
|
293
|
+
("LC_CTYPE", "'{0}'".format(lc_ctype) if lc_ctype else lc_ctype),
|
|
248
294
|
("TABLESPACE", tablespace),
|
|
249
295
|
("CONNECTION LIMIT", connection_limit),
|
|
250
296
|
):
|
|
@@ -257,26 +303,70 @@ def database(
|
|
|
257
303
|
password=psql_password,
|
|
258
304
|
host=psql_host,
|
|
259
305
|
port=psql_port,
|
|
306
|
+
database=psql_database,
|
|
260
307
|
)
|
|
261
308
|
else:
|
|
262
|
-
|
|
309
|
+
current_db = current_databases[database]
|
|
310
|
+
|
|
311
|
+
for key, value, current_value in (
|
|
312
|
+
("TEMPLATE", template, current_db.get("istemplate")),
|
|
313
|
+
("ENCODING", encoding, current_db.get("encoding")),
|
|
314
|
+
("LC_COLLATE", lc_collate, None),
|
|
315
|
+
("LC_CTYPE", lc_ctype, None),
|
|
316
|
+
):
|
|
317
|
+
if value and (current_value is None or current_value != value):
|
|
318
|
+
host.noop(
|
|
319
|
+
"postgresql database {0} already exists, skipping {1}".format(database, key)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
sql_bits = []
|
|
323
|
+
|
|
324
|
+
if owner and "owner" in current_db and current_db["owner"] != owner:
|
|
325
|
+
sql_bits.append('ALTER DATABASE "{0}" OWNER TO "{1}";'.format(database, owner))
|
|
326
|
+
|
|
327
|
+
if tablespace and "tablespace" in current_db and current_db["tablespace"] != tablespace:
|
|
328
|
+
sql_bits.append(
|
|
329
|
+
'ALTER DATABASE "{0}" SET TABLESPACE "{1}";'.format(database, tablespace)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
connection_limit
|
|
334
|
+
and "connlimit" in current_db
|
|
335
|
+
and current_db["connlimit"] != connection_limit
|
|
336
|
+
):
|
|
337
|
+
sql_bits.append(
|
|
338
|
+
'ALTER DATABASE "{0}" CONNECTION LIMIT {1};'.format(database, connection_limit)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if len(sql_bits) > 0:
|
|
342
|
+
yield make_execute_psql_command(
|
|
343
|
+
StringCommand(*sql_bits),
|
|
344
|
+
user=psql_user,
|
|
345
|
+
password=psql_password,
|
|
346
|
+
host=psql_host,
|
|
347
|
+
port=psql_port,
|
|
348
|
+
database=psql_database,
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
host.noop(
|
|
352
|
+
"postgresql database {0} already exists with the same parameters".format(database)
|
|
353
|
+
)
|
|
263
354
|
|
|
264
355
|
|
|
265
356
|
@operation(is_idempotent=False)
|
|
266
357
|
def dump(
|
|
267
358
|
dest: str,
|
|
268
|
-
database: str | None = None,
|
|
269
359
|
# Details for speaking to PostgreSQL via `psql` CLI
|
|
270
360
|
psql_user: str | None = None,
|
|
271
361
|
psql_password: str | None = None,
|
|
272
362
|
psql_host: str | None = None,
|
|
273
363
|
psql_port: int | None = None,
|
|
364
|
+
psql_database: str | None = None,
|
|
274
365
|
):
|
|
275
366
|
"""
|
|
276
367
|
Dump a PostgreSQL database into a ``.sql`` file. Requires ``pg_dump``.
|
|
277
368
|
|
|
278
369
|
+ dest: name of the file to dump the SQL to
|
|
279
|
-
+ database: name of the database to dump
|
|
280
370
|
+ psql_*: global module arguments, see above
|
|
281
371
|
|
|
282
372
|
**Example:**
|
|
@@ -286,7 +376,6 @@ def dump(
|
|
|
286
376
|
postgresql.dump(
|
|
287
377
|
name="Dump the pyinfra_stuff database",
|
|
288
378
|
dest="/tmp/pyinfra_stuff.dump",
|
|
289
|
-
database="pyinfra_stuff",
|
|
290
379
|
sudo_user="postgres",
|
|
291
380
|
)
|
|
292
381
|
|
|
@@ -295,11 +384,11 @@ def dump(
|
|
|
295
384
|
yield StringCommand(
|
|
296
385
|
make_psql_command(
|
|
297
386
|
executable="pg_dump",
|
|
298
|
-
database=database,
|
|
299
387
|
user=psql_user,
|
|
300
388
|
password=psql_password,
|
|
301
389
|
host=psql_host,
|
|
302
390
|
port=psql_port,
|
|
391
|
+
database=psql_database,
|
|
303
392
|
),
|
|
304
393
|
">",
|
|
305
394
|
QuoteString(dest),
|
|
@@ -309,18 +398,17 @@ def dump(
|
|
|
309
398
|
@operation(is_idempotent=False)
|
|
310
399
|
def load(
|
|
311
400
|
src: str,
|
|
312
|
-
database: str | None = None,
|
|
313
401
|
# Details for speaking to PostgreSQL via `psql` CLI
|
|
314
402
|
psql_user: str | None = None,
|
|
315
403
|
psql_password: str | None = None,
|
|
316
404
|
psql_host: str | None = None,
|
|
317
405
|
psql_port: int | None = None,
|
|
406
|
+
psql_database: str | None = None,
|
|
318
407
|
):
|
|
319
408
|
"""
|
|
320
409
|
Load ``.sql`` file into a database.
|
|
321
410
|
|
|
322
411
|
+ src: the filename to read from
|
|
323
|
-
+ database: name of the database to import into
|
|
324
412
|
+ psql_*: global module arguments, see above
|
|
325
413
|
|
|
326
414
|
**Example:**
|
|
@@ -330,7 +418,6 @@ def load(
|
|
|
330
418
|
postgresql.load(
|
|
331
419
|
name="Import the pyinfra_stuff dump into pyinfra_stuff_copy",
|
|
332
420
|
src="/tmp/pyinfra_stuff.dump",
|
|
333
|
-
database="pyinfra_stuff_copy",
|
|
334
421
|
sudo_user="postgres",
|
|
335
422
|
)
|
|
336
423
|
|
|
@@ -338,11 +425,11 @@ def load(
|
|
|
338
425
|
|
|
339
426
|
yield StringCommand(
|
|
340
427
|
make_psql_command(
|
|
341
|
-
database=database,
|
|
342
428
|
user=psql_user,
|
|
343
429
|
password=psql_password,
|
|
344
430
|
host=psql_host,
|
|
345
431
|
port=psql_port,
|
|
432
|
+
database=psql_database,
|
|
346
433
|
),
|
|
347
434
|
"<",
|
|
348
435
|
QuoteString(src),
|
pyinfra/operations/runit.py
CHANGED
pyinfra/operations/server.py
CHANGED
|
@@ -5,7 +5,6 @@ Linux/BSD.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
import shlex
|
|
9
8
|
from io import StringIO
|
|
10
9
|
from itertools import filterfalse, tee
|
|
11
10
|
from os import path
|
|
@@ -18,7 +17,6 @@ from pyinfra.api.util import try_int
|
|
|
18
17
|
from pyinfra.connectors.util import remove_any_sudo_askpass_file
|
|
19
18
|
from pyinfra.facts.files import Directory, FindInFile, Link
|
|
20
19
|
from pyinfra.facts.server import (
|
|
21
|
-
Crontab,
|
|
22
20
|
Groups,
|
|
23
21
|
Home,
|
|
24
22
|
Hostname,
|
|
@@ -30,6 +28,7 @@ from pyinfra.facts.server import (
|
|
|
30
28
|
Users,
|
|
31
29
|
Which,
|
|
32
30
|
)
|
|
31
|
+
from pyinfra.operations import crontab as crontab_
|
|
33
32
|
|
|
34
33
|
from . import (
|
|
35
34
|
apk,
|
|
@@ -49,7 +48,7 @@ from . import (
|
|
|
49
48
|
yum,
|
|
50
49
|
zypper,
|
|
51
50
|
)
|
|
52
|
-
from .util.files import chmod
|
|
51
|
+
from .util.files import chmod
|
|
53
52
|
|
|
54
53
|
if TYPE_CHECKING:
|
|
55
54
|
from pyinfra.api.arguments_typed import PyinfraOperation
|
|
@@ -438,7 +437,7 @@ def sysctl(
|
|
|
438
437
|
existing_sysctls = host.get_fact(Sysctl, keys=[key])
|
|
439
438
|
existing_value = existing_sysctls.get(key)
|
|
440
439
|
|
|
441
|
-
if
|
|
440
|
+
if existing_value != value:
|
|
442
441
|
yield "sysctl {0}='{1}'".format(key, string_value)
|
|
443
442
|
else:
|
|
444
443
|
host.noop("sysctl {0} is set to {1}".format(key, string_value))
|
|
@@ -588,180 +587,7 @@ def packages(
|
|
|
588
587
|
yield from package_operation._inner(packages=packages, present=present)
|
|
589
588
|
|
|
590
589
|
|
|
591
|
-
|
|
592
|
-
def crontab(
|
|
593
|
-
command: str,
|
|
594
|
-
present=True,
|
|
595
|
-
user: str | None = None,
|
|
596
|
-
cron_name: str | None = None,
|
|
597
|
-
minute="*",
|
|
598
|
-
hour="*",
|
|
599
|
-
month="*",
|
|
600
|
-
day_of_week="*",
|
|
601
|
-
day_of_month="*",
|
|
602
|
-
special_time: str | None = None,
|
|
603
|
-
interpolate_variables=False,
|
|
604
|
-
):
|
|
605
|
-
"""
|
|
606
|
-
Add/remove/update crontab entries.
|
|
607
|
-
|
|
608
|
-
+ command: the command for the cron
|
|
609
|
-
+ present: whether this cron command should exist
|
|
610
|
-
+ user: the user whose crontab to manage
|
|
611
|
-
+ cron_name: name the cronjob so future changes to the command will overwrite
|
|
612
|
-
+ minute: which minutes to execute the cron
|
|
613
|
-
+ hour: which hours to execute the cron
|
|
614
|
-
+ month: which months to execute the cron
|
|
615
|
-
+ day_of_week: which day of the week to execute the cron
|
|
616
|
-
+ day_of_month: which day of the month to execute the cron
|
|
617
|
-
+ special_time: cron "nickname" time (@reboot, @daily, etc), overrides others
|
|
618
|
-
+ interpolate_variables: whether to interpolate variables in ``command``
|
|
619
|
-
|
|
620
|
-
Cron commands:
|
|
621
|
-
Unless ``name`` is specified the command is used to identify crontab entries.
|
|
622
|
-
This means commands must be unique within a given users crontab. If you require
|
|
623
|
-
multiple identical commands, provide a different name argument for each.
|
|
624
|
-
|
|
625
|
-
Special times:
|
|
626
|
-
When provided, ``special_time`` will be used instead of any values passed in
|
|
627
|
-
for ``minute``/``hour``/``month``/``day_of_week``/``day_of_month``.
|
|
628
|
-
|
|
629
|
-
**Example:**
|
|
630
|
-
|
|
631
|
-
.. code:: python
|
|
632
|
-
|
|
633
|
-
# simple example for a crontab
|
|
634
|
-
server.crontab(
|
|
635
|
-
name="Backup /etc weekly",
|
|
636
|
-
command="/bin/tar cf /tmp/etc_bup.tar /etc",
|
|
637
|
-
name="backup_etc",
|
|
638
|
-
day_of_week=0,
|
|
639
|
-
hour=1,
|
|
640
|
-
minute=0,
|
|
641
|
-
)
|
|
642
|
-
"""
|
|
643
|
-
|
|
644
|
-
def comma_sep(value):
|
|
645
|
-
if isinstance(value, (list, tuple)):
|
|
646
|
-
return ",".join("{0}".format(v) for v in value)
|
|
647
|
-
return value
|
|
648
|
-
|
|
649
|
-
minute = comma_sep(minute)
|
|
650
|
-
hour = comma_sep(hour)
|
|
651
|
-
month = comma_sep(month)
|
|
652
|
-
day_of_week = comma_sep(day_of_week)
|
|
653
|
-
day_of_month = comma_sep(day_of_month)
|
|
654
|
-
|
|
655
|
-
crontab = host.get_fact(Crontab, user=user)
|
|
656
|
-
name_comment = "# pyinfra-name={0}".format(cron_name)
|
|
657
|
-
|
|
658
|
-
existing_crontab = crontab.get(command)
|
|
659
|
-
existing_crontab_command = command
|
|
660
|
-
existing_crontab_match = command
|
|
661
|
-
|
|
662
|
-
if not existing_crontab and cron_name: # find the crontab by name if provided
|
|
663
|
-
for cmd, details in crontab.items():
|
|
664
|
-
if not details["comments"]:
|
|
665
|
-
continue
|
|
666
|
-
if name_comment in details["comments"]:
|
|
667
|
-
existing_crontab = details
|
|
668
|
-
existing_crontab_match = cmd
|
|
669
|
-
existing_crontab_command = cmd
|
|
670
|
-
|
|
671
|
-
exists = existing_crontab is not None
|
|
672
|
-
|
|
673
|
-
edit_commands: list[str | StringCommand] = []
|
|
674
|
-
temp_filename = host.get_temp_filename()
|
|
675
|
-
|
|
676
|
-
if special_time:
|
|
677
|
-
new_crontab_line = "{0} {1}".format(special_time, command)
|
|
678
|
-
else:
|
|
679
|
-
new_crontab_line = "{minute} {hour} {day_of_month} {month} {day_of_week} {command}".format(
|
|
680
|
-
minute=minute,
|
|
681
|
-
hour=hour,
|
|
682
|
-
day_of_month=day_of_month,
|
|
683
|
-
month=month,
|
|
684
|
-
day_of_week=day_of_week,
|
|
685
|
-
command=command,
|
|
686
|
-
)
|
|
687
|
-
|
|
688
|
-
existing_crontab_match = ".*{0}.*".format(existing_crontab_match)
|
|
689
|
-
|
|
690
|
-
# Don't want the cron and it does exist? Remove the line
|
|
691
|
-
if not present and exists:
|
|
692
|
-
edit_commands.append(
|
|
693
|
-
sed_replace(
|
|
694
|
-
temp_filename,
|
|
695
|
-
existing_crontab_match,
|
|
696
|
-
"",
|
|
697
|
-
interpolate_variables=interpolate_variables,
|
|
698
|
-
),
|
|
699
|
-
)
|
|
700
|
-
|
|
701
|
-
# Want the cron but it doesn't exist? Append the line
|
|
702
|
-
elif present and not exists:
|
|
703
|
-
if cron_name:
|
|
704
|
-
if crontab: # append a blank line if cron entries already exist
|
|
705
|
-
edit_commands.append("echo '' >> {0}".format(temp_filename))
|
|
706
|
-
edit_commands.append(
|
|
707
|
-
"echo {0} >> {1}".format(
|
|
708
|
-
shlex.quote(name_comment),
|
|
709
|
-
temp_filename,
|
|
710
|
-
),
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
edit_commands.append(
|
|
714
|
-
"echo {0} >> {1}".format(
|
|
715
|
-
shlex.quote(new_crontab_line),
|
|
716
|
-
temp_filename,
|
|
717
|
-
),
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
# We have the cron and it exists, do it's details? If not, replace the line
|
|
721
|
-
elif present and exists:
|
|
722
|
-
assert existing_crontab is not None
|
|
723
|
-
if any(
|
|
724
|
-
(
|
|
725
|
-
special_time != existing_crontab.get("special_time"),
|
|
726
|
-
try_int(minute) != existing_crontab.get("minute"),
|
|
727
|
-
try_int(hour) != existing_crontab.get("hour"),
|
|
728
|
-
try_int(month) != existing_crontab.get("month"),
|
|
729
|
-
try_int(day_of_week) != existing_crontab.get("day_of_week"),
|
|
730
|
-
try_int(day_of_month) != existing_crontab.get("day_of_month"),
|
|
731
|
-
existing_crontab_command != command,
|
|
732
|
-
),
|
|
733
|
-
):
|
|
734
|
-
edit_commands.append(
|
|
735
|
-
sed_replace(
|
|
736
|
-
temp_filename,
|
|
737
|
-
existing_crontab_match,
|
|
738
|
-
new_crontab_line,
|
|
739
|
-
interpolate_variables=interpolate_variables,
|
|
740
|
-
),
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
if edit_commands:
|
|
744
|
-
crontab_args = []
|
|
745
|
-
if user:
|
|
746
|
-
crontab_args.append("-u {0}".format(user))
|
|
747
|
-
|
|
748
|
-
# List the crontab into a temporary file if it exists
|
|
749
|
-
if crontab:
|
|
750
|
-
yield "crontab -l {0} > {1}".format(" ".join(crontab_args), temp_filename)
|
|
751
|
-
|
|
752
|
-
# Now yield any edits
|
|
753
|
-
for edit_command in edit_commands:
|
|
754
|
-
yield edit_command
|
|
755
|
-
|
|
756
|
-
# Finally, use the tempfile to write a new crontab
|
|
757
|
-
yield "crontab {0} {1}".format(" ".join(crontab_args), temp_filename)
|
|
758
|
-
else:
|
|
759
|
-
host.noop(
|
|
760
|
-
"crontab {0} {1}".format(
|
|
761
|
-
command,
|
|
762
|
-
"exists" if present else "does not exist",
|
|
763
|
-
),
|
|
764
|
-
)
|
|
590
|
+
crontab = crontab_.crontab
|
|
765
591
|
|
|
766
592
|
|
|
767
593
|
@operation()
|
|
@@ -938,6 +764,7 @@ def user(
|
|
|
938
764
|
shell: str | None = None,
|
|
939
765
|
group: str | None = None,
|
|
940
766
|
groups: list[str] | None = None,
|
|
767
|
+
append=False,
|
|
941
768
|
public_keys: str | list[str] | None = None,
|
|
942
769
|
delete_keys=False,
|
|
943
770
|
ensure_home=True,
|
|
@@ -945,7 +772,6 @@ def user(
|
|
|
945
772
|
system=False,
|
|
946
773
|
uid: int | None = None,
|
|
947
774
|
comment: str | None = None,
|
|
948
|
-
add_deploy_dir=True,
|
|
949
775
|
unique=True,
|
|
950
776
|
password: str | None = None,
|
|
951
777
|
):
|
|
@@ -958,14 +784,14 @@ def user(
|
|
|
958
784
|
+ shell: the users shell
|
|
959
785
|
+ group: the users primary group
|
|
960
786
|
+ groups: the users secondary groups
|
|
787
|
+
+ append: whether to add `user` to `groups`, w/o losing membership of other groups
|
|
961
788
|
+ public_keys: list of public keys to attach to this user, ``home`` must be specified
|
|
962
789
|
+ delete_keys: whether to remove any keys not specified in ``public_keys``
|
|
963
790
|
+ ensure_home: whether to ensure the ``home`` directory exists
|
|
964
|
-
+ create_home: whether
|
|
791
|
+
+ create_home: whether user create new user home directories from the system skeleton
|
|
965
792
|
+ system: whether to create a system account
|
|
966
793
|
+ uid: use a specific userid number
|
|
967
794
|
+ comment: the user GECOS comment
|
|
968
|
-
+ add_deploy_dir: any public_key filenames are relative to the deploy directory
|
|
969
795
|
+ unique: prevent creating users with duplicate UID
|
|
970
796
|
+ password: set the encrypted password for the user
|
|
971
797
|
|
|
@@ -1105,6 +931,8 @@ def user(
|
|
|
1105
931
|
|
|
1106
932
|
# Check secondary groups, if defined
|
|
1107
933
|
if groups and set(existing_user["groups"]) != set(groups):
|
|
934
|
+
if append:
|
|
935
|
+
args.append("-a")
|
|
1108
936
|
args.append("-G {0}".format(",".join(groups)))
|
|
1109
937
|
|
|
1110
938
|
if comment and existing_user["comment"] != comment:
|
|
@@ -1,38 +1,60 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
1
4
|
from pyinfra.api import OperationError
|
|
2
5
|
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
@dataclasses.dataclass
|
|
8
|
+
class ContainerSpec:
|
|
9
|
+
image: str = ""
|
|
10
|
+
ports: List[str] = dataclasses.field(default_factory=list)
|
|
11
|
+
networks: List[str] = dataclasses.field(default_factory=list)
|
|
12
|
+
volumes: List[str] = dataclasses.field(default_factory=list)
|
|
13
|
+
env_vars: List[str] = dataclasses.field(default_factory=list)
|
|
14
|
+
pull_always: bool = False
|
|
15
|
+
|
|
16
|
+
def container_create_args(self):
|
|
17
|
+
args = []
|
|
18
|
+
for network in self.networks:
|
|
19
|
+
args.append("--network {0}".format(network))
|
|
6
20
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
volumes = kwargs["volumes"] if kwargs["volumes"] else []
|
|
10
|
-
env_vars = kwargs["env_vars"] if kwargs["env_vars"] else []
|
|
21
|
+
for port in self.ports:
|
|
22
|
+
args.append("-p {0}".format(port))
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
for volume in self.volumes:
|
|
25
|
+
args.append("-v {0}".format(volume))
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
for env_var in self.env_vars:
|
|
28
|
+
args.append("-e {0}".format(env_var))
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
if self.pull_always:
|
|
31
|
+
args.append("--pull always")
|
|
19
32
|
|
|
20
|
-
|
|
21
|
-
command.append("-p {0}".format(port))
|
|
33
|
+
args.append(self.image)
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
command.append("-v {0}".format(volume))
|
|
35
|
+
return args
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
def diff_from_inspect(self, inspect_dict: Dict[str, Any]) -> List[str]:
|
|
38
|
+
# TODO(@minor-fixes): Diff output of "docker inspect" against this spec
|
|
39
|
+
# to determine if the container needs to be recreated. Currently, this
|
|
40
|
+
# function will never recreate when attributes change, which is
|
|
41
|
+
# consistent with prior behavior.
|
|
42
|
+
del inspect_dict
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _create_container(**kwargs):
|
|
47
|
+
if "spec" not in kwargs:
|
|
48
|
+
raise OperationError("missing 1 required argument: 'spec'")
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
command.append("--pull always")
|
|
50
|
+
spec = kwargs["spec"]
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
if not spec.image:
|
|
53
|
+
raise OperationError("Docker image not specified")
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
command = [
|
|
56
|
+
"docker container create --name {0}".format(kwargs["container"])
|
|
57
|
+
] + spec.container_create_args()
|
|
36
58
|
|
|
37
59
|
return " ".join(command)
|
|
38
60
|
|