pyinfra 3.1.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 +9 -2
- pyinfra/api/deploy.py +4 -2
- pyinfra/api/host.py +5 -3
- pyinfra/connectors/docker.py +17 -6
- pyinfra/connectors/sshuserclient/client.py +26 -14
- pyinfra/facts/apk.py +3 -1
- pyinfra/facts/apt.py +60 -0
- 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/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/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 +27 -20
- pyinfra/operations/crontab.py +189 -0
- pyinfra/operations/docker.py +13 -12
- pyinfra/operations/files.py +18 -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 +15 -11
- pyinfra/operations/runit.py +2 -0
- pyinfra/operations/server.py +3 -177
- pyinfra/operations/zfs.py +3 -3
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/METADATA +11 -12
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/RECORD +45 -36
- pyinfra_cli/inventory.py +26 -9
- pyinfra_cli/prints.py +18 -3
- pyinfra_cli/util.py +3 -0
- tests/test_cli/test_cli_deploy.py +15 -13
- tests/test_cli/test_cli_inventory.py +53 -0
- tests/test_connectors/test_sshuserclient.py +68 -1
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/WHEEL +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
|
@@ -321,7 +328,7 @@ def update(cache_time: int | None = None):
|
|
|
321
328
|
_update = update # noqa: E305
|
|
322
329
|
|
|
323
330
|
|
|
324
|
-
@operation(
|
|
331
|
+
@operation()
|
|
325
332
|
def upgrade(auto_remove: bool = False):
|
|
326
333
|
"""
|
|
327
334
|
Upgrades all apt packages.
|
|
@@ -349,13 +356,13 @@ def upgrade(auto_remove: bool = False):
|
|
|
349
356
|
if auto_remove:
|
|
350
357
|
command.append("--autoremove")
|
|
351
358
|
|
|
352
|
-
yield
|
|
359
|
+
yield from _simulate_then_perform(" ".join(command))
|
|
353
360
|
|
|
354
361
|
|
|
355
362
|
_upgrade = upgrade # noqa: E305 (for use below where update is a kwarg)
|
|
356
363
|
|
|
357
364
|
|
|
358
|
-
@operation(
|
|
365
|
+
@operation()
|
|
359
366
|
def dist_upgrade():
|
|
360
367
|
"""
|
|
361
368
|
Updates all apt packages, employing dist-upgrade.
|
|
@@ -369,7 +376,7 @@ def dist_upgrade():
|
|
|
369
376
|
)
|
|
370
377
|
"""
|
|
371
378
|
|
|
372
|
-
yield
|
|
379
|
+
yield from _simulate_then_perform("dist-upgrade")
|
|
373
380
|
|
|
374
381
|
|
|
375
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
|
|
pyinfra/operations/files.py
CHANGED
|
@@ -51,6 +51,7 @@ from pyinfra.facts.files import (
|
|
|
51
51
|
Md5File,
|
|
52
52
|
Sha1File,
|
|
53
53
|
Sha256File,
|
|
54
|
+
Sha384File,
|
|
54
55
|
)
|
|
55
56
|
from pyinfra.facts.server import Date, Which
|
|
56
57
|
|
|
@@ -67,6 +68,7 @@ def download(
|
|
|
67
68
|
mode: str | None = None,
|
|
68
69
|
cache_time: int | None = None,
|
|
69
70
|
force=False,
|
|
71
|
+
sha384sum: str | None = None,
|
|
70
72
|
sha256sum: str | None = None,
|
|
71
73
|
sha1sum: str | None = None,
|
|
72
74
|
md5sum: str | None = None,
|
|
@@ -84,6 +86,7 @@ def download(
|
|
|
84
86
|
+ mode: permissions of the files
|
|
85
87
|
+ cache_time: if the file exists already, re-download after this time (in seconds)
|
|
86
88
|
+ force: always download the file, even if it already exists
|
|
89
|
+
+ sha384sum: sha384 hash to checksum the downloaded file against
|
|
87
90
|
+ sha256sum: sha256 hash to checksum the downloaded file against
|
|
88
91
|
+ sha1sum: sha1 hash to checksum the downloaded file against
|
|
89
92
|
+ md5sum: md5 hash to checksum the downloaded file against
|
|
@@ -135,6 +138,10 @@ def download(
|
|
|
135
138
|
if sha256sum != host.get_fact(Sha256File, path=dest):
|
|
136
139
|
download = True
|
|
137
140
|
|
|
141
|
+
if sha384sum:
|
|
142
|
+
if sha384sum != host.get_fact(Sha384File, path=dest):
|
|
143
|
+
download = True
|
|
144
|
+
|
|
138
145
|
if md5sum:
|
|
139
146
|
if md5sum != host.get_fact(Md5File, path=dest):
|
|
140
147
|
download = True
|
|
@@ -211,6 +218,17 @@ def download(
|
|
|
211
218
|
QuoteString("SHA256 did not match!"),
|
|
212
219
|
)
|
|
213
220
|
|
|
221
|
+
if sha384sum:
|
|
222
|
+
yield make_formatted_string_command(
|
|
223
|
+
(
|
|
224
|
+
"(( sha384sum {0} 2> /dev/null || shasum -a 384 {0} ) "
|
|
225
|
+
"| grep {1}) || ( echo {2} && exit 1 )"
|
|
226
|
+
),
|
|
227
|
+
QuoteString(dest),
|
|
228
|
+
sha384sum,
|
|
229
|
+
QuoteString("SHA384 did not match!"),
|
|
230
|
+
)
|
|
231
|
+
|
|
214
232
|
if md5sum:
|
|
215
233
|
yield make_formatted_string_command(
|
|
216
234
|
(
|
pyinfra/operations/git.py
CHANGED
|
@@ -16,24 +16,40 @@ from .util.files import chown, unix_path_join
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@operation()
|
|
19
|
-
def config(key: str, value: str, multi_value=False, repo: str | None = None):
|
|
19
|
+
def config(key: str, value: str, multi_value=False, repo: str | None = None, system=False):
|
|
20
20
|
"""
|
|
21
|
-
Manage git config
|
|
21
|
+
Manage git config at repository, user or system level.
|
|
22
22
|
|
|
23
23
|
+ key: the key of the config to ensure
|
|
24
24
|
+ value: the value this key should have
|
|
25
25
|
+ multi_value: Add the value rather than set it for settings that can have multiple values
|
|
26
26
|
+ repo: specify the git repo path to edit local config (defaults to global)
|
|
27
|
+
+ system: whether, when ``repo`` is unspecified, to work at system level (or default to global)
|
|
27
28
|
|
|
28
|
-
**
|
|
29
|
+
**Examples:**
|
|
29
30
|
|
|
30
31
|
.. code:: python
|
|
31
32
|
|
|
32
33
|
git.config(
|
|
33
|
-
name="
|
|
34
|
+
name="Always prune specified repo",
|
|
35
|
+
key="fetch.prune",
|
|
36
|
+
value="true",
|
|
37
|
+
repo="/usr/local/src/pyinfra",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
git.config(
|
|
41
|
+
name="Ensure user name is set for all repos of specified user",
|
|
34
42
|
key="user.name",
|
|
35
43
|
value="Anon E. Mouse",
|
|
36
|
-
|
|
44
|
+
_sudo=True,
|
|
45
|
+
_sudo_user="anon"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
git.config(
|
|
49
|
+
name="Ensure same date format for all users",
|
|
50
|
+
key="log.date",
|
|
51
|
+
value="iso",
|
|
52
|
+
system=True
|
|
37
53
|
)
|
|
38
54
|
|
|
39
55
|
"""
|
|
@@ -41,14 +57,14 @@ def config(key: str, value: str, multi_value=False, repo: str | None = None):
|
|
|
41
57
|
existing_config = {}
|
|
42
58
|
|
|
43
59
|
if not repo:
|
|
44
|
-
existing_config = host.get_fact(GitConfig)
|
|
60
|
+
existing_config = host.get_fact(GitConfig, system=system)
|
|
45
61
|
|
|
46
62
|
# Only get the config if the repo exists at this stage
|
|
47
63
|
elif host.get_fact(Directory, path=unix_path_join(repo, ".git")):
|
|
48
64
|
existing_config = host.get_fact(GitConfig, repo=repo)
|
|
49
65
|
|
|
50
66
|
if repo is None:
|
|
51
|
-
base_command = "git config --global"
|
|
67
|
+
base_command = "git config" + (" --system" if system else " --global")
|
|
52
68
|
else:
|
|
53
69
|
base_command = "cd {0} && git config --local".format(repo)
|
|
54
70
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manage packages on OpenWrt using opkg
|
|
3
|
+
+ ``update`` - update local copy of package information
|
|
4
|
+
+ ``packages`` - install and remove packages
|
|
5
|
+
|
|
6
|
+
see https://openwrt.org/docs/guide-user/additional-software/opkg
|
|
7
|
+
OpenWrt recommends against upgrading all packages thus there is no ``opkg.upgrade`` function
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import List, Union
|
|
11
|
+
|
|
12
|
+
from pyinfra import host
|
|
13
|
+
from pyinfra.api import StringCommand, operation
|
|
14
|
+
from pyinfra.facts.opkg import OpkgPackages
|
|
15
|
+
from pyinfra.operations.util.packaging import ensure_packages
|
|
16
|
+
|
|
17
|
+
EQUALS = "="
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@operation(is_idempotent=False)
|
|
21
|
+
def update():
|
|
22
|
+
"""
|
|
23
|
+
Update the local opkg information.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
yield StringCommand("opkg update")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_update = update
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@operation()
|
|
33
|
+
def packages(
|
|
34
|
+
packages: Union[str, List[str]] = "",
|
|
35
|
+
present: bool = True,
|
|
36
|
+
latest: bool = False,
|
|
37
|
+
update: bool = True,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Add/remove/update opkg packages.
|
|
41
|
+
|
|
42
|
+
+ packages: package or list of packages to that must/must not be present
|
|
43
|
+
+ present: whether the package(s) should be installed (default True) or removed
|
|
44
|
+
+ latest: whether to attempt to upgrade the specified package(s) (default False)
|
|
45
|
+
+ update: run ``opkg update`` before installing packages (default True)
|
|
46
|
+
|
|
47
|
+
Not Supported:
|
|
48
|
+
Opkg does not support version pinning, i.e. ``<pkg>=<version>`` is not allowed
|
|
49
|
+
and will cause an exception.
|
|
50
|
+
|
|
51
|
+
**Examples:**
|
|
52
|
+
|
|
53
|
+
.. code:: python
|
|
54
|
+
|
|
55
|
+
# Ensure packages are installed∂ (will not force package upgrade)
|
|
56
|
+
opkg.packages(['asterisk', 'vim'], name="Install Asterisk and Vim")
|
|
57
|
+
|
|
58
|
+
# Install the latest versions of packages (always check)
|
|
59
|
+
opkg.packages(
|
|
60
|
+
'vim',
|
|
61
|
+
latest=True,
|
|
62
|
+
name="Ensure we have the latest version of Vim"
|
|
63
|
+
)
|
|
64
|
+
"""
|
|
65
|
+
if str(packages) == "" or (
|
|
66
|
+
isinstance(packages, list) and (len(packages) < 1 or all(len(p) < 1 for p in packages))
|
|
67
|
+
):
|
|
68
|
+
host.noop("empty or invalid package list provided to opkg.packages")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
pkg_list = packages if isinstance(packages, list) else [packages]
|
|
72
|
+
have_equals = ",".join([pkg.split(EQUALS)[0] for pkg in pkg_list if EQUALS in pkg])
|
|
73
|
+
if len(have_equals) > 0:
|
|
74
|
+
raise ValueError(f"opkg does not support version pinning but found for: '{have_equals}'")
|
|
75
|
+
|
|
76
|
+
if update:
|
|
77
|
+
yield from _update._inner()
|
|
78
|
+
|
|
79
|
+
yield from ensure_packages(
|
|
80
|
+
host,
|
|
81
|
+
pkg_list,
|
|
82
|
+
host.get_fact(OpkgPackages),
|
|
83
|
+
present,
|
|
84
|
+
install_command="opkg install",
|
|
85
|
+
upgrade_command="opkg upgrade",
|
|
86
|
+
uninstall_command="opkg remove",
|
|
87
|
+
latest=latest,
|
|
88
|
+
)
|