pyinfra 3.1__py2.py3-none-any.whl → 3.2__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 +10 -3
- pyinfra/api/deploy.py +12 -2
- pyinfra/api/host.py +7 -4
- pyinfra/connectors/chroot.py +1 -1
- pyinfra/connectors/docker.py +17 -6
- pyinfra/connectors/local.py +1 -1
- pyinfra/connectors/ssh.py +3 -0
- pyinfra/connectors/sshuserclient/client.py +26 -14
- pyinfra/facts/apk.py +3 -1
- pyinfra/facts/apt.py +62 -2
- pyinfra/facts/crontab.py +190 -0
- pyinfra/facts/docker.py +6 -0
- pyinfra/facts/efibootmgr.py +108 -0
- pyinfra/facts/files.py +93 -6
- pyinfra/facts/git.py +3 -2
- pyinfra/facts/hardware.py +1 -0
- pyinfra/facts/mysql.py +1 -2
- pyinfra/facts/opkg.py +233 -0
- pyinfra/facts/pipx.py +74 -0
- pyinfra/facts/podman.py +47 -0
- pyinfra/facts/postgres.py +2 -0
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +39 -77
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/zfs.py +22 -19
- pyinfra/local.py +3 -2
- pyinfra/operations/apt.py +29 -21
- pyinfra/operations/crontab.py +189 -0
- pyinfra/operations/docker.py +13 -12
- pyinfra/operations/files.py +20 -2
- pyinfra/operations/git.py +48 -9
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pip.py +3 -2
- pyinfra/operations/pipx.py +90 -0
- pyinfra/operations/postgres.py +15 -11
- pyinfra/operations/runit.py +2 -0
- pyinfra/operations/server.py +4 -178
- pyinfra/operations/zfs.py +14 -14
- {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/METADATA +11 -12
- {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/RECORD +52 -43
- pyinfra_cli/inventory.py +26 -9
- pyinfra_cli/prints.py +18 -3
- pyinfra_cli/util.py +5 -2
- tests/test_cli/test_cli_deploy.py +15 -13
- tests/test_cli/test_cli_exceptions.py +2 -2
- tests/test_cli/test_cli_inventory.py +53 -0
- tests/test_cli/test_cli_util.py +2 -4
- tests/test_connectors/test_sshuserclient.py +68 -1
- {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/WHEEL +0 -0
- {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/top_level.txt +0 -0
pyinfra/facts/server.py
CHANGED
|
@@ -5,14 +5,15 @@ import re
|
|
|
5
5
|
import shutil
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from tempfile import mkdtemp
|
|
8
|
-
from typing import Dict, List, Optional
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
9
|
|
|
10
10
|
from dateutil.parser import parse as parse_date
|
|
11
11
|
from distro import distro
|
|
12
|
-
from typing_extensions import
|
|
12
|
+
from typing_extensions import TypedDict
|
|
13
13
|
|
|
14
14
|
from pyinfra.api import FactBase, ShortFactBase
|
|
15
15
|
from pyinfra.api.util import try_int
|
|
16
|
+
from pyinfra.facts import crontab
|
|
16
17
|
|
|
17
18
|
ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
|
18
19
|
|
|
@@ -31,8 +32,7 @@ class Home(FactBase[Optional[str]]):
|
|
|
31
32
|
Returns the home directory of the given user, or the current user if no user is given.
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
def command(user=""):
|
|
35
|
+
def command(self, user=""):
|
|
36
36
|
return f"echo ~{user}"
|
|
37
37
|
|
|
38
38
|
|
|
@@ -123,8 +123,7 @@ class Command(FactBase[str]):
|
|
|
123
123
|
Returns the raw output lines of a given command.
|
|
124
124
|
"""
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
def command(command):
|
|
126
|
+
def command(self, command):
|
|
128
127
|
return command
|
|
129
128
|
|
|
130
129
|
|
|
@@ -133,8 +132,7 @@ class Which(FactBase[Optional[str]]):
|
|
|
133
132
|
Returns the path of a given command according to `command -v`, if available.
|
|
134
133
|
"""
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
def command(command):
|
|
135
|
+
def command(self, command):
|
|
138
136
|
return "command -v {0} || true".format(command)
|
|
139
137
|
|
|
140
138
|
|
|
@@ -307,6 +305,36 @@ class LsbRelease(FactBase):
|
|
|
307
305
|
return items
|
|
308
306
|
|
|
309
307
|
|
|
308
|
+
class OsRelease(FactBase):
|
|
309
|
+
"""
|
|
310
|
+
Returns a dictionary of release information stored in ``/etc/os-release``.
|
|
311
|
+
|
|
312
|
+
.. code:: python
|
|
313
|
+
|
|
314
|
+
{
|
|
315
|
+
"name": "EndeavourOS",
|
|
316
|
+
"pretty_name": "EndeavourOS",
|
|
317
|
+
"id": "endeavouros",
|
|
318
|
+
"id_like": "arch",
|
|
319
|
+
"build_id": "2024.06.25",
|
|
320
|
+
...
|
|
321
|
+
}
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def command(self):
|
|
325
|
+
return "cat /etc/os-release"
|
|
326
|
+
|
|
327
|
+
def process(self, output):
|
|
328
|
+
items = {}
|
|
329
|
+
|
|
330
|
+
for line in output:
|
|
331
|
+
if "=" in line:
|
|
332
|
+
key, value = line.split("=", 1)
|
|
333
|
+
items[key.strip().lower()] = value.strip().strip('"')
|
|
334
|
+
|
|
335
|
+
return items
|
|
336
|
+
|
|
337
|
+
|
|
310
338
|
class Sysctl(FactBase):
|
|
311
339
|
"""
|
|
312
340
|
Returns a dictionary of sysctl settings and values.
|
|
@@ -377,75 +405,9 @@ class Groups(FactBase[List[str]]):
|
|
|
377
405
|
return groups
|
|
378
406
|
|
|
379
407
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
month: NotRequired[Union[int, str]]
|
|
384
|
-
day_of_month: NotRequired[Union[int, str]]
|
|
385
|
-
day_of_week: NotRequired[Union[int, str]]
|
|
386
|
-
comments: Optional[list[str]]
|
|
387
|
-
special_time: NotRequired[str]
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
class Crontab(FactBase[Dict[str, CrontabDict]]):
|
|
391
|
-
"""
|
|
392
|
-
Returns a dictionary of cron command -> execution time.
|
|
393
|
-
|
|
394
|
-
.. code:: python
|
|
395
|
-
|
|
396
|
-
{
|
|
397
|
-
"/path/to/command": {
|
|
398
|
-
"minute": "*",
|
|
399
|
-
"hour": "*",
|
|
400
|
-
"month": "*",
|
|
401
|
-
"day_of_month": "*",
|
|
402
|
-
"day_of_week": "*",
|
|
403
|
-
},
|
|
404
|
-
"echo another command": {
|
|
405
|
-
"special_time": "@daily",
|
|
406
|
-
},
|
|
407
|
-
}
|
|
408
|
-
"""
|
|
409
|
-
|
|
410
|
-
default = dict
|
|
411
|
-
|
|
412
|
-
def requires_command(self, user=None) -> str:
|
|
413
|
-
return "crontab"
|
|
414
|
-
|
|
415
|
-
def command(self, user=None):
|
|
416
|
-
if user:
|
|
417
|
-
return "crontab -l -u {0} || true".format(user)
|
|
418
|
-
return "crontab -l || true"
|
|
419
|
-
|
|
420
|
-
def process(self, output):
|
|
421
|
-
crons: dict[str, CrontabDict] = {}
|
|
422
|
-
current_comments = []
|
|
423
|
-
|
|
424
|
-
for line in output:
|
|
425
|
-
line = line.strip()
|
|
426
|
-
if not line or line.startswith("#"):
|
|
427
|
-
current_comments.append(line)
|
|
428
|
-
continue
|
|
429
|
-
|
|
430
|
-
if line.startswith("@"):
|
|
431
|
-
special_time, command = line.split(None, 1)
|
|
432
|
-
crons[command] = {
|
|
433
|
-
"special_time": special_time,
|
|
434
|
-
"comments": current_comments,
|
|
435
|
-
}
|
|
436
|
-
else:
|
|
437
|
-
minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
|
|
438
|
-
crons[command] = {
|
|
439
|
-
"minute": try_int(minute),
|
|
440
|
-
"hour": try_int(hour),
|
|
441
|
-
"month": try_int(month),
|
|
442
|
-
"day_of_month": try_int(day_of_month),
|
|
443
|
-
"day_of_week": try_int(day_of_week),
|
|
444
|
-
"comments": current_comments,
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
current_comments = []
|
|
448
|
-
return crons
|
|
408
|
+
# for compatibility
|
|
409
|
+
CrontabDict = crontab.CrontabDict
|
|
410
|
+
Crontab = crontab.Crontab
|
|
449
411
|
|
|
450
412
|
|
|
451
413
|
class Users(FactBase):
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# from https://stackoverflow.com/a/60708339, but with a few modifications
|
|
2
|
+
from __future__ import annotations # for | in type hints
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
units = {
|
|
7
|
+
"B": 1,
|
|
8
|
+
"KB": 10**3,
|
|
9
|
+
"MB": 10**6,
|
|
10
|
+
"GB": 10**9,
|
|
11
|
+
"TB": 10**12,
|
|
12
|
+
"KIB": 2**10,
|
|
13
|
+
"MIB": 2**20,
|
|
14
|
+
"GIB": 2**30,
|
|
15
|
+
"TIB": 2**40,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_human_readable_size(size: str) -> int:
|
|
20
|
+
size = size.upper()
|
|
21
|
+
if not re.match(r" ", size):
|
|
22
|
+
size = re.sub(r"([KMGT]?I?[B])", r" \1", size)
|
|
23
|
+
number, unit = [string.strip() for string in size.split()]
|
|
24
|
+
return int(float(number) * units[unit])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_size(size: str | int) -> int:
|
|
28
|
+
if isinstance(size, int):
|
|
29
|
+
return size
|
|
30
|
+
return parse_human_readable_size(size)
|
pyinfra/facts/zfs.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Gather information about ZFS filesystems.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from pyinfra.api import FactBase, ShortFactBase
|
|
@@ -15,43 +15,46 @@ def _process_zfs_props_table(output):
|
|
|
15
15
|
return datasets
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class ZfsPools(FactBase):
|
|
19
19
|
def command(self):
|
|
20
20
|
return "zpool get -H all"
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
def process(output):
|
|
22
|
+
def process(self, output):
|
|
24
23
|
return _process_zfs_props_table(output)
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
class
|
|
26
|
+
class ZfsDatasets(FactBase):
|
|
28
27
|
def command(self):
|
|
29
28
|
return "zfs get -H all"
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
def process(output):
|
|
30
|
+
def process(self, output):
|
|
33
31
|
return _process_zfs_props_table(output)
|
|
34
32
|
|
|
35
33
|
|
|
36
|
-
class
|
|
37
|
-
fact =
|
|
34
|
+
class ZfsFilesystems(ShortFactBase):
|
|
35
|
+
fact = ZfsDatasets
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
def process_data(data):
|
|
37
|
+
def process_data(self, data):
|
|
41
38
|
return {name: props for name, props in data.items() if props.get("type") == "filesystem"}
|
|
42
39
|
|
|
43
40
|
|
|
44
|
-
class
|
|
45
|
-
fact =
|
|
41
|
+
class ZfsSnapshots(ShortFactBase):
|
|
42
|
+
fact = ZfsDatasets
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
def process_data(data):
|
|
44
|
+
def process_data(self, data):
|
|
49
45
|
return {name: props for name, props in data.items() if props.get("type") == "snapshot"}
|
|
50
46
|
|
|
51
47
|
|
|
52
|
-
class
|
|
53
|
-
fact =
|
|
48
|
+
class ZfsVolumes(ShortFactBase):
|
|
49
|
+
fact = ZfsDatasets
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
def process_data(data):
|
|
51
|
+
def process_data(self, data):
|
|
57
52
|
return {name: props for name, props in data.items() if props.get("type") == "volume"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# TODO: remove these in v4! Or flip the convention and remove all the other fact prefixes!
|
|
56
|
+
Pools = ZfsPools
|
|
57
|
+
Datasets = ZfsDatasets
|
|
58
|
+
Filesystems = ZfsFilesystems
|
|
59
|
+
Snapshots = ZfsSnapshots
|
|
60
|
+
Volumes = ZfsVolumes
|
pyinfra/local.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from os import path
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
4
|
import click
|
|
4
5
|
|
|
@@ -10,7 +11,7 @@ from pyinfra.connectors.util import run_local_process
|
|
|
10
11
|
from pyinfra.context import ctx_state
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def include(filename: str):
|
|
14
|
+
def include(filename: str, data: Optional[dict] = None):
|
|
14
15
|
"""
|
|
15
16
|
Executes a local python file within the ``pyinfra.state.cwd``
|
|
16
17
|
directory.
|
|
@@ -33,7 +34,7 @@ def include(filename: str):
|
|
|
33
34
|
|
|
34
35
|
from pyinfra_cli.util import exec_file
|
|
35
36
|
|
|
36
|
-
with host.deploy(path.relpath(filename, state.cwd), None,
|
|
37
|
+
with host.deploy(path.relpath(filename, state.cwd), None, data, in_deploy=False):
|
|
37
38
|
exec_file(filename)
|
|
38
39
|
|
|
39
40
|
# One potential solution to the above is to add local as an actual
|
pyinfra/operations/apt.py
CHANGED
|
@@ -9,7 +9,13 @@ from urllib.parse import urlparse
|
|
|
9
9
|
|
|
10
10
|
from pyinfra import host
|
|
11
11
|
from pyinfra.api import OperationError, operation
|
|
12
|
-
from pyinfra.facts.apt import
|
|
12
|
+
from pyinfra.facts.apt import (
|
|
13
|
+
AptKeys,
|
|
14
|
+
AptSources,
|
|
15
|
+
SimulateOperationWillChange,
|
|
16
|
+
noninteractive_apt,
|
|
17
|
+
parse_apt_repo,
|
|
18
|
+
)
|
|
13
19
|
from pyinfra.facts.deb import DebPackage, DebPackages
|
|
14
20
|
from pyinfra.facts.files import File
|
|
15
21
|
from pyinfra.facts.gpg import GpgKey
|
|
@@ -21,21 +27,22 @@ from .util.packaging import ensure_packages
|
|
|
21
27
|
APT_UPDATE_FILENAME = "/var/lib/apt/periodic/update-success-stamp"
|
|
22
28
|
|
|
23
29
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
def _simulate_then_perform(command: str):
|
|
31
|
+
changes = host.get_fact(SimulateOperationWillChange, command)
|
|
32
|
+
|
|
33
|
+
if not changes:
|
|
34
|
+
# Simulating apt-get command failed, so the actual
|
|
35
|
+
# operation will probably fail too:
|
|
36
|
+
yield noninteractive_apt(command)
|
|
37
|
+
elif (
|
|
38
|
+
changes["upgraded"] == 0
|
|
39
|
+
and changes["newly_installed"] == 0
|
|
40
|
+
and changes["removed"] == 0
|
|
41
|
+
and changes["not_upgraded"] == 0
|
|
42
|
+
):
|
|
43
|
+
host.noop(f"{command} skipped, no changes would be performed")
|
|
44
|
+
else:
|
|
45
|
+
yield noninteractive_apt(command)
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
@operation()
|
|
@@ -53,7 +60,8 @@ def key(src: str | None = None, keyserver: str | None = None, keyid: str | list[
|
|
|
53
60
|
.. warning::
|
|
54
61
|
``apt-key`` is deprecated in Debian, it is recommended NOT to use this
|
|
55
62
|
operation and instead follow the instructions here:
|
|
56
|
-
|
|
63
|
+
|
|
64
|
+
https://wiki.debian.org/DebianRepository/UseThirdParty
|
|
57
65
|
|
|
58
66
|
**Examples:**
|
|
59
67
|
|
|
@@ -320,7 +328,7 @@ def update(cache_time: int | None = None):
|
|
|
320
328
|
_update = update # noqa: E305
|
|
321
329
|
|
|
322
330
|
|
|
323
|
-
@operation(
|
|
331
|
+
@operation()
|
|
324
332
|
def upgrade(auto_remove: bool = False):
|
|
325
333
|
"""
|
|
326
334
|
Upgrades all apt packages.
|
|
@@ -348,13 +356,13 @@ def upgrade(auto_remove: bool = False):
|
|
|
348
356
|
if auto_remove:
|
|
349
357
|
command.append("--autoremove")
|
|
350
358
|
|
|
351
|
-
yield
|
|
359
|
+
yield from _simulate_then_perform(" ".join(command))
|
|
352
360
|
|
|
353
361
|
|
|
354
362
|
_upgrade = upgrade # noqa: E305 (for use below where update is a kwarg)
|
|
355
363
|
|
|
356
364
|
|
|
357
|
-
@operation(
|
|
365
|
+
@operation()
|
|
358
366
|
def dist_upgrade():
|
|
359
367
|
"""
|
|
360
368
|
Updates all apt packages, employing dist-upgrade.
|
|
@@ -368,7 +376,7 @@ def dist_upgrade():
|
|
|
368
376
|
)
|
|
369
377
|
"""
|
|
370
378
|
|
|
371
|
-
yield
|
|
379
|
+
yield from _simulate_then_perform("dist-upgrade")
|
|
372
380
|
|
|
373
381
|
|
|
374
382
|
@operation()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
|
|
5
|
+
from pyinfra import host
|
|
6
|
+
from pyinfra.api import StringCommand, operation
|
|
7
|
+
from pyinfra.api.util import try_int
|
|
8
|
+
from pyinfra.facts.crontab import Crontab, CrontabFile
|
|
9
|
+
from pyinfra.operations.util.files import sed_replace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@operation()
|
|
13
|
+
def crontab(
|
|
14
|
+
command: str,
|
|
15
|
+
present=True,
|
|
16
|
+
user: str | None = None,
|
|
17
|
+
cron_name: str | None = None,
|
|
18
|
+
minute="*",
|
|
19
|
+
hour="*",
|
|
20
|
+
month="*",
|
|
21
|
+
day_of_week="*",
|
|
22
|
+
day_of_month="*",
|
|
23
|
+
special_time: str | None = None,
|
|
24
|
+
interpolate_variables=False,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Add/remove/update crontab entries.
|
|
28
|
+
|
|
29
|
+
+ command: the command for the cron
|
|
30
|
+
+ present: whether this cron command should exist
|
|
31
|
+
+ user: the user whose crontab to manage
|
|
32
|
+
+ cron_name: name the cronjob so future changes to the command will overwrite
|
|
33
|
+
+ modify_cron_name: modify the cron name
|
|
34
|
+
+ minute: which minutes to execute the cron
|
|
35
|
+
+ hour: which hours to execute the cron
|
|
36
|
+
+ month: which months to execute the cron
|
|
37
|
+
+ day_of_week: which day of the week to execute the cron
|
|
38
|
+
+ day_of_month: which day of the month to execute the cron
|
|
39
|
+
+ special_time: cron "nickname" time (@reboot, @daily, etc), overrides others
|
|
40
|
+
+ interpolate_variables: whether to interpolate variables in ``command``
|
|
41
|
+
|
|
42
|
+
Cron commands:
|
|
43
|
+
Unless ``name`` is specified the command is used to identify crontab entries.
|
|
44
|
+
This means commands must be unique within a given users crontab. If you require
|
|
45
|
+
multiple identical commands, provide a different name argument for each.
|
|
46
|
+
|
|
47
|
+
Special times:
|
|
48
|
+
When provided, ``special_time`` will be used instead of any values passed in
|
|
49
|
+
for ``minute``/``hour``/``month``/``day_of_week``/``day_of_month``.
|
|
50
|
+
|
|
51
|
+
**Example:**
|
|
52
|
+
|
|
53
|
+
.. code:: python
|
|
54
|
+
|
|
55
|
+
# simple example for a crontab
|
|
56
|
+
crontab.crontab(
|
|
57
|
+
name="Backup /etc weekly",
|
|
58
|
+
command="/bin/tar cf /tmp/etc_bup.tar /etc",
|
|
59
|
+
name="backup_etc",
|
|
60
|
+
day_of_week=0,
|
|
61
|
+
hour=1,
|
|
62
|
+
minute=0,
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def comma_sep(value):
|
|
67
|
+
if isinstance(value, (list, tuple)):
|
|
68
|
+
return ",".join("{0}".format(v) for v in value)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
minute = comma_sep(minute)
|
|
72
|
+
hour = comma_sep(hour)
|
|
73
|
+
month = comma_sep(month)
|
|
74
|
+
day_of_week = comma_sep(day_of_week)
|
|
75
|
+
day_of_month = comma_sep(day_of_month)
|
|
76
|
+
|
|
77
|
+
ctb0: CrontabFile | dict = host.get_fact(Crontab, user=user)
|
|
78
|
+
# facts from test are in dict
|
|
79
|
+
if isinstance(ctb0, dict):
|
|
80
|
+
ctb = CrontabFile(ctb0)
|
|
81
|
+
else:
|
|
82
|
+
ctb = ctb0
|
|
83
|
+
name_comment = "# pyinfra-name={0}".format(cron_name)
|
|
84
|
+
|
|
85
|
+
existing_crontab = ctb.get_command(command=command, name=cron_name)
|
|
86
|
+
existing_crontab_command = existing_crontab["command"] if existing_crontab else command
|
|
87
|
+
existing_crontab_match = existing_crontab["command"] if existing_crontab else command
|
|
88
|
+
|
|
89
|
+
exists = existing_crontab is not None
|
|
90
|
+
exists_name = existing_crontab is not None and name_comment in existing_crontab.get(
|
|
91
|
+
"comments", ""
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
edit_commands: list[str | StringCommand] = []
|
|
95
|
+
temp_filename = host.get_temp_filename()
|
|
96
|
+
|
|
97
|
+
if special_time:
|
|
98
|
+
new_crontab_line = "{0} {1}".format(special_time, command)
|
|
99
|
+
else:
|
|
100
|
+
new_crontab_line = "{minute} {hour} {day_of_month} {month} {day_of_week} {command}".format(
|
|
101
|
+
minute=minute,
|
|
102
|
+
hour=hour,
|
|
103
|
+
day_of_month=day_of_month,
|
|
104
|
+
month=month,
|
|
105
|
+
day_of_week=day_of_week,
|
|
106
|
+
command=command,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
existing_crontab_match = ".*{0}.*".format(existing_crontab_match)
|
|
110
|
+
|
|
111
|
+
# Don't want the cron and it does exist? Remove the line
|
|
112
|
+
if not present and exists:
|
|
113
|
+
edit_commands.append(
|
|
114
|
+
sed_replace(
|
|
115
|
+
temp_filename,
|
|
116
|
+
existing_crontab_match,
|
|
117
|
+
"",
|
|
118
|
+
interpolate_variables=interpolate_variables,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Want the cron but it doesn't exist? Append the line
|
|
123
|
+
elif present and not exists:
|
|
124
|
+
print("present", present, "exists", exists)
|
|
125
|
+
if ctb: # append a blank line if cron entries already exist
|
|
126
|
+
edit_commands.append("echo '' >> {0}".format(temp_filename))
|
|
127
|
+
if cron_name:
|
|
128
|
+
edit_commands.append(
|
|
129
|
+
"echo {0} >> {1}".format(
|
|
130
|
+
shlex.quote(name_comment),
|
|
131
|
+
temp_filename,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
edit_commands.append(
|
|
136
|
+
"echo {0} >> {1}".format(
|
|
137
|
+
shlex.quote(new_crontab_line),
|
|
138
|
+
temp_filename,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# We have the cron and it exists, do it's details? If not, replace the line
|
|
143
|
+
elif present and exists:
|
|
144
|
+
assert existing_crontab is not None
|
|
145
|
+
if any(
|
|
146
|
+
(
|
|
147
|
+
exists_name != (cron_name is not None),
|
|
148
|
+
special_time != existing_crontab.get("special_time"),
|
|
149
|
+
try_int(minute) != existing_crontab.get("minute"),
|
|
150
|
+
try_int(hour) != existing_crontab.get("hour"),
|
|
151
|
+
try_int(month) != existing_crontab.get("month"),
|
|
152
|
+
try_int(day_of_week) != existing_crontab.get("day_of_week"),
|
|
153
|
+
try_int(day_of_month) != existing_crontab.get("day_of_month"),
|
|
154
|
+
existing_crontab_command != command,
|
|
155
|
+
),
|
|
156
|
+
):
|
|
157
|
+
if not exists_name and cron_name:
|
|
158
|
+
new_crontab_line = f"{name_comment}\n{new_crontab_line}"
|
|
159
|
+
edit_commands.append(
|
|
160
|
+
sed_replace(
|
|
161
|
+
temp_filename,
|
|
162
|
+
existing_crontab_match,
|
|
163
|
+
new_crontab_line,
|
|
164
|
+
interpolate_variables=interpolate_variables,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if edit_commands:
|
|
169
|
+
crontab_args = []
|
|
170
|
+
if user:
|
|
171
|
+
crontab_args.append("-u {0}".format(user))
|
|
172
|
+
|
|
173
|
+
# List the crontab into a temporary file if it exists
|
|
174
|
+
if ctb:
|
|
175
|
+
yield "crontab -l {0} > {1}".format(" ".join(crontab_args), temp_filename)
|
|
176
|
+
|
|
177
|
+
# Now yield any edits
|
|
178
|
+
for edit_command in edit_commands:
|
|
179
|
+
yield edit_command
|
|
180
|
+
|
|
181
|
+
# Finally, use the tempfile to write a new crontab
|
|
182
|
+
yield "crontab {0} {1}".format(" ".join(crontab_args), temp_filename)
|
|
183
|
+
else:
|
|
184
|
+
host.noop(
|
|
185
|
+
"crontab {0} {1}".format(
|
|
186
|
+
command,
|
|
187
|
+
"exists" if present else "does not exist",
|
|
188
|
+
),
|
|
189
|
+
)
|
pyinfra/operations/docker.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Manager Docker
|
|
2
|
+
Manager Docker containers, volumes and networks. These operations allow you to manage Docker from
|
|
3
|
+
the view of the current inventory host. See the :doc:`../connectors/docker` to use Docker containers
|
|
4
|
+
as inventory directly.
|
|
3
5
|
"""
|
|
4
6
|
|
|
5
7
|
from pyinfra import host
|
|
@@ -30,9 +32,9 @@ def container(
|
|
|
30
32
|
+ networks: network list to attach on container
|
|
31
33
|
+ ports: port list to expose
|
|
32
34
|
+ volumes: volume list to map on container
|
|
33
|
-
+ env_vars: environment
|
|
35
|
+
+ env_vars: environment variable list to inject on container
|
|
34
36
|
+ pull_always: force image pull
|
|
35
|
-
+ force: remove a
|
|
37
|
+
+ force: remove a container with same name and create a new one
|
|
36
38
|
+ present: whether the container should be up and running
|
|
37
39
|
+ start: start or stop the container
|
|
38
40
|
|
|
@@ -125,7 +127,7 @@ def image(image, present=True):
|
|
|
125
127
|
Manage Docker images
|
|
126
128
|
|
|
127
129
|
+ image: Image and tag ex: nginx:alpine
|
|
128
|
-
+ present: whether the Docker image should
|
|
130
|
+
+ present: whether the Docker image should exist
|
|
129
131
|
|
|
130
132
|
**Examples:**
|
|
131
133
|
|
|
@@ -188,7 +190,7 @@ def volume(volume, driver="", labels=None, present=True):
|
|
|
188
190
|
if present:
|
|
189
191
|
|
|
190
192
|
if existent_volume:
|
|
191
|
-
host.noop("Volume
|
|
193
|
+
host.noop("Volume already exists!")
|
|
192
194
|
return
|
|
193
195
|
|
|
194
196
|
yield handle_docker(
|
|
@@ -231,8 +233,8 @@ def network(
|
|
|
231
233
|
"""
|
|
232
234
|
Manage docker networks
|
|
233
235
|
|
|
234
|
-
+
|
|
235
|
-
+ driver:
|
|
236
|
+
+ network: Network name
|
|
237
|
+
+ driver: Network driver ex: bridge or overlay
|
|
236
238
|
+ gateway: IPv4 or IPv6 Gateway for the master subnet
|
|
237
239
|
+ ip_range: Allocate container ip from a sub-range
|
|
238
240
|
+ ipam_driver: IP Address Management Driver
|
|
@@ -251,8 +253,7 @@ def network(
|
|
|
251
253
|
|
|
252
254
|
# Create Docker network
|
|
253
255
|
docker.network(
|
|
254
|
-
|
|
255
|
-
network_name="nginx",
|
|
256
|
+
network="nginx",
|
|
256
257
|
attachable=True,
|
|
257
258
|
present=True,
|
|
258
259
|
)
|
|
@@ -261,7 +262,7 @@ def network(
|
|
|
261
262
|
|
|
262
263
|
if present:
|
|
263
264
|
if existent_network:
|
|
264
|
-
host.noop("
|
|
265
|
+
host.noop("Network {0} already exists!".format(network))
|
|
265
266
|
return
|
|
266
267
|
|
|
267
268
|
yield handle_docker(
|
|
@@ -284,12 +285,12 @@ def network(
|
|
|
284
285
|
|
|
285
286
|
else:
|
|
286
287
|
if existent_network is None:
|
|
287
|
-
host.noop("
|
|
288
|
+
host.noop("Network {0} does not exist!".format(network))
|
|
288
289
|
return
|
|
289
290
|
|
|
290
291
|
yield handle_docker(
|
|
291
292
|
resource="network",
|
|
292
|
-
command="
|
|
293
|
+
command="remove",
|
|
293
294
|
network=network,
|
|
294
295
|
)
|
|
295
296
|
|