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/facts/crontab.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Optional, TypedDict, Union
|
|
3
|
+
|
|
4
|
+
from typing_extensions import NotRequired, override
|
|
5
|
+
|
|
6
|
+
from pyinfra.api import FactBase
|
|
7
|
+
from pyinfra.api.util import try_int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CrontabDict(TypedDict):
|
|
11
|
+
command: NotRequired[str]
|
|
12
|
+
# handles cases like CRON_TZ=UTC
|
|
13
|
+
env: NotRequired[str]
|
|
14
|
+
minute: NotRequired[Union[int, str]]
|
|
15
|
+
hour: NotRequired[Union[int, str]]
|
|
16
|
+
month: NotRequired[Union[int, str]]
|
|
17
|
+
day_of_month: NotRequired[Union[int, str]]
|
|
18
|
+
day_of_week: NotRequired[Union[int, str]]
|
|
19
|
+
comments: NotRequired[List[str]]
|
|
20
|
+
special_time: NotRequired[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# for compatibility, also keeps a dict of command -> crontab dict
|
|
24
|
+
class CrontabFile:
|
|
25
|
+
commands: List[CrontabDict]
|
|
26
|
+
|
|
27
|
+
def __init__(self, input_dict: Optional[Dict[str, CrontabDict]] = None):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.commands = []
|
|
30
|
+
if input_dict:
|
|
31
|
+
for command, others in input_dict.items():
|
|
32
|
+
val = others.copy()
|
|
33
|
+
val["command"] = command
|
|
34
|
+
self.add_item(val)
|
|
35
|
+
|
|
36
|
+
def add_item(self, item: CrontabDict):
|
|
37
|
+
self.commands.append(item)
|
|
38
|
+
|
|
39
|
+
def __len__(self):
|
|
40
|
+
return len(self.commands)
|
|
41
|
+
|
|
42
|
+
def __bool__(self):
|
|
43
|
+
return len(self) > 0
|
|
44
|
+
|
|
45
|
+
def items(self):
|
|
46
|
+
return {item.get("command") or item.get("env"): item for item in self.commands}
|
|
47
|
+
|
|
48
|
+
def get_command(
|
|
49
|
+
self, command: Optional[str] = None, name: Optional[str] = None
|
|
50
|
+
) -> Optional[CrontabDict]:
|
|
51
|
+
assert command or name, "Either command or name must be provided"
|
|
52
|
+
|
|
53
|
+
name_comment = "# pyinfra-name={0}".format(name)
|
|
54
|
+
for cmd in self.commands:
|
|
55
|
+
if cmd.get("command") == command:
|
|
56
|
+
return cmd
|
|
57
|
+
if cmd.get("comments") and name_comment in cmd["comments"]:
|
|
58
|
+
return cmd
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def get_env(self, env: str) -> Optional[CrontabDict]:
|
|
62
|
+
for cmd in self.commands:
|
|
63
|
+
if cmd.get("env") == env:
|
|
64
|
+
return cmd
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def get(self, item: str) -> Optional[CrontabDict]:
|
|
68
|
+
return self.get_command(command=item, name=item) or self.get_env(item)
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, item) -> Optional[CrontabDict]:
|
|
71
|
+
return self.get(item)
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
def __repr__(self):
|
|
75
|
+
return f"CrontabResult({self.commands})"
|
|
76
|
+
|
|
77
|
+
# noinspection PyMethodMayBeStatic
|
|
78
|
+
def format_item(self, item: CrontabDict):
|
|
79
|
+
lines = []
|
|
80
|
+
for comment in item.get("comments", []):
|
|
81
|
+
lines.append(comment)
|
|
82
|
+
|
|
83
|
+
if "env" in item:
|
|
84
|
+
lines.append(item["env"])
|
|
85
|
+
elif "special_time" in item:
|
|
86
|
+
lines.append(f"{item['special_time']} {item['command']}")
|
|
87
|
+
else:
|
|
88
|
+
lines.append(
|
|
89
|
+
f"{item['minute']} {item['hour']} "
|
|
90
|
+
f"{item['day_of_month']} {item['month']} {item['day_of_week']} "
|
|
91
|
+
f"{item['command']}"
|
|
92
|
+
)
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
def __str__(self):
|
|
97
|
+
return "\n".join(self.format_item(item) for item in self.commands)
|
|
98
|
+
|
|
99
|
+
def to_json(self):
|
|
100
|
+
return self.commands
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_crontab_env_re = re.compile(r"^\s*([A-Z_]+)=(.*)$")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Crontab(FactBase[CrontabFile]):
|
|
107
|
+
"""
|
|
108
|
+
Returns a dictionary of CrontabFile.
|
|
109
|
+
|
|
110
|
+
.. code:: python
|
|
111
|
+
|
|
112
|
+
# CrontabFile.items()
|
|
113
|
+
{
|
|
114
|
+
"/path/to/command": {
|
|
115
|
+
"minute": "*",
|
|
116
|
+
"hour": "*",
|
|
117
|
+
"month": "*",
|
|
118
|
+
"day_of_month": "*",
|
|
119
|
+
"day_of_week": "*",
|
|
120
|
+
},
|
|
121
|
+
"echo another command": {
|
|
122
|
+
"special_time": "@daily",
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
# or CrontabFile.to_json()
|
|
126
|
+
[
|
|
127
|
+
{
|
|
128
|
+
command: "/path/to/command",
|
|
129
|
+
minute: "*",
|
|
130
|
+
hour: "*",
|
|
131
|
+
month: "*",
|
|
132
|
+
day_of_month: "*",
|
|
133
|
+
day_of_week: "*",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"command": "echo another command
|
|
137
|
+
"special_time": "@daily",
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
default = CrontabFile
|
|
143
|
+
|
|
144
|
+
@override
|
|
145
|
+
def requires_command(self, user=None) -> str:
|
|
146
|
+
return "crontab"
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
def command(self, user=None):
|
|
150
|
+
if user:
|
|
151
|
+
return "crontab -l -u {0} || true".format(user)
|
|
152
|
+
return "crontab -l || true"
|
|
153
|
+
|
|
154
|
+
@override
|
|
155
|
+
def process(self, output):
|
|
156
|
+
crons = CrontabFile()
|
|
157
|
+
current_comments = []
|
|
158
|
+
|
|
159
|
+
for line in output:
|
|
160
|
+
line = line.strip()
|
|
161
|
+
if not line or line.startswith("#"):
|
|
162
|
+
current_comments.append(line)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if line.startswith("@"):
|
|
166
|
+
special_time, command = line.split(None, 1)
|
|
167
|
+
item = CrontabDict(
|
|
168
|
+
command=command,
|
|
169
|
+
special_time=special_time,
|
|
170
|
+
comments=current_comments,
|
|
171
|
+
)
|
|
172
|
+
crons.add_item(item)
|
|
173
|
+
|
|
174
|
+
elif _crontab_env_re.match(line):
|
|
175
|
+
# handle environment variables
|
|
176
|
+
item = CrontabDict(
|
|
177
|
+
env=line,
|
|
178
|
+
comments=current_comments,
|
|
179
|
+
)
|
|
180
|
+
crons.add_item(item)
|
|
181
|
+
else:
|
|
182
|
+
minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
|
|
183
|
+
item = CrontabDict(
|
|
184
|
+
command=command,
|
|
185
|
+
minute=try_int(minute),
|
|
186
|
+
hour=try_int(hour),
|
|
187
|
+
month=try_int(month),
|
|
188
|
+
day_of_month=try_int(day_of_month),
|
|
189
|
+
day_of_week=try_int(day_of_week),
|
|
190
|
+
comments=current_comments,
|
|
191
|
+
)
|
|
192
|
+
crons.add_item(item)
|
|
193
|
+
|
|
194
|
+
current_comments = []
|
|
195
|
+
return crons
|
pyinfra/facts/deb.py
CHANGED
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
import shlex
|
|
5
5
|
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
|
|
6
8
|
from pyinfra.api import FactBase
|
|
7
9
|
|
|
8
10
|
from .util.packaging import parse_packages
|
|
@@ -16,9 +18,11 @@ class DebArch(FactBase):
|
|
|
16
18
|
Returns the architecture string used in apt repository sources, eg ``amd64``.
|
|
17
19
|
"""
|
|
18
20
|
|
|
21
|
+
@override
|
|
19
22
|
def command(self) -> str:
|
|
20
23
|
return "dpkg --print-architecture"
|
|
21
24
|
|
|
25
|
+
@override
|
|
22
26
|
def requires_command(self) -> str:
|
|
23
27
|
return "dpkg"
|
|
24
28
|
|
|
@@ -34,9 +38,11 @@ class DebPackages(FactBase):
|
|
|
34
38
|
}
|
|
35
39
|
"""
|
|
36
40
|
|
|
41
|
+
@override
|
|
37
42
|
def command(self) -> str:
|
|
38
43
|
return "dpkg -l"
|
|
39
44
|
|
|
45
|
+
@override
|
|
40
46
|
def requires_command(self) -> str:
|
|
41
47
|
return "dpkg"
|
|
42
48
|
|
|
@@ -47,6 +53,7 @@ class DebPackages(FactBase):
|
|
|
47
53
|
DEB_PACKAGE_VERSION_REGEX,
|
|
48
54
|
)
|
|
49
55
|
|
|
56
|
+
@override
|
|
50
57
|
def process(self, output):
|
|
51
58
|
return parse_packages(self.regex, output)
|
|
52
59
|
|
|
@@ -61,14 +68,17 @@ class DebPackage(FactBase):
|
|
|
61
68
|
"version": r"^Version:\s+({0})$".format(DEB_PACKAGE_VERSION_REGEX),
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
@override
|
|
64
72
|
def requires_command(self, package) -> str:
|
|
65
73
|
return "dpkg"
|
|
66
74
|
|
|
75
|
+
@override
|
|
67
76
|
def command(self, package):
|
|
68
77
|
return "! test -e {0} && (dpkg -s {0} 2>/dev/null || true) || dpkg -I {0}".format(
|
|
69
78
|
shlex.quote(package)
|
|
70
79
|
)
|
|
71
80
|
|
|
81
|
+
@override
|
|
72
82
|
def process(self, output):
|
|
73
83
|
data = {}
|
|
74
84
|
|
pyinfra/facts/dnf.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
3
5
|
from pyinfra.api import FactBase
|
|
4
6
|
|
|
5
7
|
from .util import make_cat_files_command
|
|
@@ -23,6 +25,7 @@ class DnfRepositories(FactBase):
|
|
|
23
25
|
]
|
|
24
26
|
"""
|
|
25
27
|
|
|
28
|
+
@override
|
|
26
29
|
def command(self) -> str:
|
|
27
30
|
return make_cat_files_command(
|
|
28
31
|
"/etc/dnf.conf",
|
|
@@ -30,10 +33,12 @@ class DnfRepositories(FactBase):
|
|
|
30
33
|
"/etc/yum.repos.d/*.repo",
|
|
31
34
|
)
|
|
32
35
|
|
|
36
|
+
@override
|
|
33
37
|
def requires_command(self) -> str:
|
|
34
38
|
return "dnf"
|
|
35
39
|
|
|
36
40
|
default = list
|
|
37
41
|
|
|
42
|
+
@override
|
|
38
43
|
def process(self, output):
|
|
39
44
|
return parse_yum_repositories(output)
|
pyinfra/facts/docker.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Facts about Docker containers, volumes and networks. These facts give you information from the view
|
|
3
|
+
of the current inventory host. See the :doc:`../connectors/docker` to use Docker containers as
|
|
4
|
+
inventory directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
3
9
|
import json
|
|
4
10
|
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
5
13
|
from pyinfra.api import FactBase
|
|
6
14
|
|
|
7
15
|
|
|
@@ -10,9 +18,11 @@ class DockerFactBase(FactBase):
|
|
|
10
18
|
|
|
11
19
|
docker_type: str
|
|
12
20
|
|
|
21
|
+
@override
|
|
13
22
|
def requires_command(self, *args, **kwargs) -> str:
|
|
14
23
|
return "docker"
|
|
15
24
|
|
|
25
|
+
@override
|
|
16
26
|
def process(self, output):
|
|
17
27
|
output = "".join(output)
|
|
18
28
|
return json.loads(output)
|
|
@@ -23,6 +33,7 @@ class DockerSystemInfo(DockerFactBase):
|
|
|
23
33
|
Returns ``docker system info`` output in JSON format.
|
|
24
34
|
"""
|
|
25
35
|
|
|
36
|
+
@override
|
|
26
37
|
def command(self) -> str:
|
|
27
38
|
return 'docker system info --format="{{json .}}"'
|
|
28
39
|
|
|
@@ -36,6 +47,7 @@ class DockerContainers(DockerFactBase):
|
|
|
36
47
|
Returns ``docker inspect`` output for all Docker containers.
|
|
37
48
|
"""
|
|
38
49
|
|
|
50
|
+
@override
|
|
39
51
|
def command(self) -> str:
|
|
40
52
|
return "docker container inspect `docker ps -qa`"
|
|
41
53
|
|
|
@@ -45,6 +57,7 @@ class DockerImages(DockerFactBase):
|
|
|
45
57
|
Returns ``docker inspect`` output for all Docker images.
|
|
46
58
|
"""
|
|
47
59
|
|
|
60
|
+
@override
|
|
48
61
|
def command(self) -> str:
|
|
49
62
|
return "docker image inspect `docker images -q`"
|
|
50
63
|
|
|
@@ -54,6 +67,7 @@ class DockerNetworks(DockerFactBase):
|
|
|
54
67
|
Returns ``docker inspect`` output for all Docker networks.
|
|
55
68
|
"""
|
|
56
69
|
|
|
70
|
+
@override
|
|
57
71
|
def command(self) -> str:
|
|
58
72
|
return "docker network inspect `docker network ls -q`"
|
|
59
73
|
|
|
@@ -63,6 +77,7 @@ class DockerNetworks(DockerFactBase):
|
|
|
63
77
|
|
|
64
78
|
|
|
65
79
|
class DockerSingleMixin(DockerFactBase):
|
|
80
|
+
@override
|
|
66
81
|
def command(self, object_id):
|
|
67
82
|
return "docker {0} inspect {1} 2>&- || true".format(
|
|
68
83
|
self.docker_type,
|
|
@@ -99,6 +114,7 @@ class DockerVolumes(DockerFactBase):
|
|
|
99
114
|
Returns ``docker inspect`` output for all Docker volumes.
|
|
100
115
|
"""
|
|
101
116
|
|
|
117
|
+
@override
|
|
102
118
|
def command(self) -> str:
|
|
103
119
|
return "docker volume inspect `docker volume ls -q`"
|
|
104
120
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple, TypedDict
|
|
4
|
+
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
7
|
+
from pyinfra.api import FactBase
|
|
8
|
+
|
|
9
|
+
BootEntry = Tuple[bool, str]
|
|
10
|
+
EFIBootMgrInfoDict = TypedDict(
|
|
11
|
+
"EFIBootMgrInfoDict",
|
|
12
|
+
{
|
|
13
|
+
"BootNext": Optional[int],
|
|
14
|
+
"BootCurrent": Optional[int],
|
|
15
|
+
"Timeout": Optional[int],
|
|
16
|
+
"BootOrder": Optional[List[int]],
|
|
17
|
+
"Entries": Dict[int, BootEntry],
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EFIBootMgr(FactBase[Optional[EFIBootMgrInfoDict]]):
|
|
23
|
+
"""
|
|
24
|
+
Returns information about the UEFI boot variables:
|
|
25
|
+
|
|
26
|
+
.. code:: python
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
"BootNext": 6,
|
|
30
|
+
"BootCurrent": 6,
|
|
31
|
+
"Timeout": 0,
|
|
32
|
+
"BootOrder": [1,4,3],
|
|
33
|
+
"Entries": {
|
|
34
|
+
1: (True, "myefi1"),
|
|
35
|
+
2: (False, "myefi2.efi"),
|
|
36
|
+
3: (True, "myefi3.efi"),
|
|
37
|
+
4: (True, "grub2.efi"),
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def requires_command(self, *args: Any, **kwargs: Any) -> str:
|
|
44
|
+
return "efibootmgr"
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def command(self) -> str:
|
|
48
|
+
# FIXME: Use '|| true' to properly handle the case where
|
|
49
|
+
# 'efibootmgr' is run on a non-UEFI system
|
|
50
|
+
return "efibootmgr || true"
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
def process(self, output: Iterable[str]) -> Optional[EFIBootMgrInfoDict]:
|
|
54
|
+
# This parsing code closely follows the printing code of efibootmgr
|
|
55
|
+
# at <https://github.com/rhboot/efibootmgr/blob/main/src/efibootmgr.c#L2020-L2048>
|
|
56
|
+
|
|
57
|
+
info: EFIBootMgrInfoDict = {
|
|
58
|
+
"BootNext": None,
|
|
59
|
+
"BootCurrent": None,
|
|
60
|
+
"Timeout": None,
|
|
61
|
+
"BootOrder": [],
|
|
62
|
+
"Entries": {},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
output = iter(output)
|
|
66
|
+
|
|
67
|
+
line: Optional[str] = next(output, None)
|
|
68
|
+
|
|
69
|
+
if line is None:
|
|
70
|
+
# efibootmgr run on a non-UEFI system, likely printed
|
|
71
|
+
# "EFI variables are not supported on this system."
|
|
72
|
+
# to stderr
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# 1. Maybe have BootNext
|
|
76
|
+
if line and line.startswith("BootNext: "):
|
|
77
|
+
info["BootNext"] = int(line[len("BootNext: ") :], 16)
|
|
78
|
+
line = next(output, None)
|
|
79
|
+
|
|
80
|
+
# 2. Maybe have BootCurrent
|
|
81
|
+
if line and line.startswith("BootCurrent: "):
|
|
82
|
+
info["BootCurrent"] = int(line[len("BootCurrent: ") :], 16)
|
|
83
|
+
line = next(output, None)
|
|
84
|
+
|
|
85
|
+
# 3. Maybe have Timeout
|
|
86
|
+
if line and line.startswith("Timeout: "):
|
|
87
|
+
info["Timeout"] = int(line[len("Timeout: ") : -len(" seconds")])
|
|
88
|
+
line = next(output, None)
|
|
89
|
+
|
|
90
|
+
# 4. `show_order`
|
|
91
|
+
if line and line.startswith("BootOrder: "):
|
|
92
|
+
entries = line[len("BootOrder: ") :]
|
|
93
|
+
info["BootOrder"] = list(map(lambda x: int(x, 16), entries.split(",")))
|
|
94
|
+
line = next(output, None)
|
|
95
|
+
|
|
96
|
+
# 5. `show_vars`: The actual boot entries
|
|
97
|
+
while line is not None and line.startswith("Boot"):
|
|
98
|
+
number = int(line[4:8], 16)
|
|
99
|
+
|
|
100
|
+
# Entries marked with a * are active
|
|
101
|
+
active = line[8:9] == "*"
|
|
102
|
+
|
|
103
|
+
# TODO: Maybe split and parse (name vs. arguments ?), might require --verbose ?
|
|
104
|
+
entry = line[10:]
|
|
105
|
+
info["Entries"][number] = (active, entry)
|
|
106
|
+
line = next(output, None)
|
|
107
|
+
|
|
108
|
+
# 6. `show_mirror`
|
|
109
|
+
# Currently not implemented, since I haven't actually encountered this in the wild.
|
|
110
|
+
if line is not None:
|
|
111
|
+
raise ValueError(f"Unexpected line '{line}' while parsing")
|
|
112
|
+
|
|
113
|
+
return info
|
pyinfra/facts/files.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
The files facts provide information about the filesystem and it's contents on the target host.
|
|
3
|
+
|
|
4
|
+
Facts need to be imported before use, eg
|
|
5
|
+
|
|
6
|
+
from pyinfra.facts.files import File
|
|
3
7
|
"""
|
|
4
8
|
|
|
5
9
|
from __future__ import annotations
|
|
@@ -10,11 +14,13 @@ import stat
|
|
|
10
14
|
from datetime import datetime
|
|
11
15
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
12
16
|
|
|
13
|
-
from typing_extensions import Literal, NotRequired, TypedDict
|
|
17
|
+
from typing_extensions import Literal, NotRequired, TypedDict, override
|
|
14
18
|
|
|
19
|
+
from pyinfra.api import StringCommand
|
|
15
20
|
from pyinfra.api.command import QuoteString, make_formatted_string_command
|
|
16
21
|
from pyinfra.api.facts import FactBase
|
|
17
22
|
from pyinfra.api.util import try_int
|
|
23
|
+
from pyinfra.facts.util.units import parse_size
|
|
18
24
|
|
|
19
25
|
LINUX_STAT_COMMAND = "stat -c 'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"
|
|
20
26
|
BSD_STAT_COMMAND = "stat -f 'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"
|
|
@@ -109,6 +115,7 @@ class File(FactBase[Union[FileDict, Literal[False], None]]):
|
|
|
109
115
|
|
|
110
116
|
type = "file"
|
|
111
117
|
|
|
118
|
+
@override
|
|
112
119
|
def command(self, path):
|
|
113
120
|
if path.startswith("~/"):
|
|
114
121
|
# Do not quote leading tilde to ensure that it gets properly expanded by the shell
|
|
@@ -127,6 +134,7 @@ class File(FactBase[Union[FileDict, Literal[False], None]]):
|
|
|
127
134
|
bsd_stat_command=BSD_STAT_COMMAND,
|
|
128
135
|
)
|
|
129
136
|
|
|
137
|
+
@override
|
|
130
138
|
def process(self, output) -> Union[FileDict, Literal[False], None]:
|
|
131
139
|
match = re.match(STAT_REGEX, output[0])
|
|
132
140
|
if not match:
|
|
@@ -231,6 +239,7 @@ class HashFileFactBase(FactBaseOptionalStr):
|
|
|
231
239
|
_raw_cmd: str
|
|
232
240
|
_regexes: Tuple[str, str]
|
|
233
241
|
|
|
242
|
+
@override
|
|
234
243
|
def __init_subclass__(cls, digits: int, cmds: List[str], **kwargs) -> None:
|
|
235
244
|
super().__init_subclass__(**kwargs)
|
|
236
245
|
|
|
@@ -247,10 +256,12 @@ class HashFileFactBase(FactBaseOptionalStr):
|
|
|
247
256
|
r"^%s\s+\(%%s\)\s+=\s+([a-fA-F0-9]{%d})$" % (hash_name, digits),
|
|
248
257
|
)
|
|
249
258
|
|
|
259
|
+
@override
|
|
250
260
|
def command(self, path):
|
|
251
261
|
self.path = path
|
|
252
262
|
return make_formatted_string_command(self._raw_cmd, QuoteString(path))
|
|
253
263
|
|
|
264
|
+
@override
|
|
254
265
|
def process(self, output) -> Optional[str]:
|
|
255
266
|
output = output[0]
|
|
256
267
|
escaped_path = re.escape(self.path)
|
|
@@ -274,6 +285,12 @@ class Sha256File(HashFileFactBase, digits=64, cmds=["sha256sum", "shasum -a 256"
|
|
|
274
285
|
"""
|
|
275
286
|
|
|
276
287
|
|
|
288
|
+
class Sha384File(HashFileFactBase, digits=96, cmds=["sha384sum", "shasum -a 384", "sha384"]):
|
|
289
|
+
"""
|
|
290
|
+
Returns a SHA384 hash of a file, or ``None`` if the file does not exist.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
|
|
277
294
|
class Md5File(HashFileFactBase, digits=32, cmds=["md5sum", "md5"]):
|
|
278
295
|
"""
|
|
279
296
|
Returns an MD5 hash of a file, or ``None`` if the file does not exist.
|
|
@@ -286,6 +303,7 @@ class FindInFile(FactBase):
|
|
|
286
303
|
lines if the file exists, and ``None`` if the file does not.
|
|
287
304
|
"""
|
|
288
305
|
|
|
306
|
+
@override
|
|
289
307
|
def command(self, path, pattern, interpolate_variables=False):
|
|
290
308
|
self.exists_flag = "__pyinfra_exists_{0}".format(path)
|
|
291
309
|
|
|
@@ -304,6 +322,7 @@ class FindInFile(FactBase):
|
|
|
304
322
|
QuoteString(self.exists_flag),
|
|
305
323
|
)
|
|
306
324
|
|
|
325
|
+
@override
|
|
307
326
|
def process(self, output):
|
|
308
327
|
# If output is the special string: no matches, so return an empty list;
|
|
309
328
|
# this allows us to differentiate between no matches in an existing file
|
|
@@ -319,15 +338,96 @@ class FindFilesBase(FactBase):
|
|
|
319
338
|
default = list
|
|
320
339
|
type_flag: str
|
|
321
340
|
|
|
341
|
+
@override
|
|
322
342
|
def process(self, output):
|
|
323
343
|
return output
|
|
324
344
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
345
|
+
@override
|
|
346
|
+
def command(
|
|
347
|
+
self,
|
|
348
|
+
path: str,
|
|
349
|
+
size: Optional[str | int] = None,
|
|
350
|
+
min_size: Optional[str | int] = None,
|
|
351
|
+
max_size: Optional[str | int] = None,
|
|
352
|
+
maxdepth: Optional[int] = None,
|
|
353
|
+
fname: Optional[str] = None,
|
|
354
|
+
iname: Optional[str] = None,
|
|
355
|
+
regex: Optional[str] = None,
|
|
356
|
+
args: Optional[List[str]] = None,
|
|
357
|
+
quote_path=True,
|
|
358
|
+
):
|
|
359
|
+
"""
|
|
360
|
+
@param path: the path to start the search from
|
|
361
|
+
@param size: exact size in bytes or human-readable format.
|
|
362
|
+
GB means 1e9 bytes, GiB means 2^30 bytes
|
|
363
|
+
@param min_size: minimum size in bytes or human-readable format
|
|
364
|
+
@param max_size: maximum size in bytes or human-readable format
|
|
365
|
+
@param maxdepth: maximum depth to descend to
|
|
366
|
+
@param name: True if the last component of the pathname being examined matches pattern.
|
|
367
|
+
Special shell pattern matching characters (“[”, “]”, “*”, and “?”)
|
|
368
|
+
may be used as part of pattern.
|
|
369
|
+
These characters may be matched explicitly
|
|
370
|
+
by escaping them with a backslash (“\\”).
|
|
371
|
+
|
|
372
|
+
@param iname: Like -name, but the match is case insensitive.
|
|
373
|
+
@param regex: True if the whole path of the file matches pattern using regular expression.
|
|
374
|
+
@param args: additional arguments to pass to find
|
|
375
|
+
@param quote_path: if the path should be quoted
|
|
376
|
+
@return:
|
|
377
|
+
"""
|
|
378
|
+
if args is None:
|
|
379
|
+
args = []
|
|
380
|
+
|
|
381
|
+
def maybe_quote(value):
|
|
382
|
+
return QuoteString(value) if quote_path else value
|
|
383
|
+
|
|
384
|
+
command = [
|
|
385
|
+
"find",
|
|
386
|
+
maybe_quote(path),
|
|
387
|
+
"-type",
|
|
388
|
+
self.type_flag,
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
"""
|
|
392
|
+
Why we need special handling for size:
|
|
393
|
+
https://unix.stackexchange.com/questions/275925/why-does-find-size-1g-not-find-any-files
|
|
394
|
+
In short, 'c' means bytes, without it, it means 512-byte blocks.
|
|
395
|
+
If we use any units other than 'c', it has a weird rounding behavior,
|
|
396
|
+
and is implementation-specific. So, we always use 'c'
|
|
397
|
+
"""
|
|
398
|
+
if "-size" not in args:
|
|
399
|
+
if min_size is not None:
|
|
400
|
+
command.append("-size")
|
|
401
|
+
command.append("+{0}c".format(parse_size(min_size)))
|
|
402
|
+
|
|
403
|
+
if max_size is not None:
|
|
404
|
+
command.append("-size")
|
|
405
|
+
command.append("-{0}c".format(parse_size(max_size)))
|
|
406
|
+
|
|
407
|
+
if size is not None:
|
|
408
|
+
command.append("-size")
|
|
409
|
+
command.append("{0}c".format(size))
|
|
410
|
+
|
|
411
|
+
if maxdepth is not None and "-maxdepth" not in args:
|
|
412
|
+
command.append("-maxdepth")
|
|
413
|
+
command.append("{0}".format(maxdepth))
|
|
414
|
+
|
|
415
|
+
if fname is not None and "-fname" not in args:
|
|
416
|
+
command.append("-name")
|
|
417
|
+
command.append(maybe_quote(fname))
|
|
418
|
+
|
|
419
|
+
if iname is not None and "-iname" not in args:
|
|
420
|
+
command.append("-iname")
|
|
421
|
+
command.append(maybe_quote(iname))
|
|
422
|
+
|
|
423
|
+
if regex is not None and "-regex" not in args:
|
|
424
|
+
command.append("-regex")
|
|
425
|
+
command.append(maybe_quote(regex))
|
|
426
|
+
|
|
427
|
+
command.append("||")
|
|
428
|
+
command.append("true")
|
|
429
|
+
|
|
430
|
+
return StringCommand(*command)
|
|
331
431
|
|
|
332
432
|
|
|
333
433
|
class FindFiles(FindFilesBase):
|
|
@@ -359,15 +459,18 @@ class Flags(FactBase):
|
|
|
359
459
|
Returns a list of the file flags set for the specified file or directory.
|
|
360
460
|
"""
|
|
361
461
|
|
|
462
|
+
@override
|
|
362
463
|
def requires_command(self, path) -> str:
|
|
363
464
|
return "chflags" # don't try to retrieve them if we can't set them
|
|
364
465
|
|
|
466
|
+
@override
|
|
365
467
|
def command(self, path):
|
|
366
468
|
return make_formatted_string_command(
|
|
367
469
|
"! test -e {0} || stat -f %Sf {0}",
|
|
368
470
|
QuoteString(path),
|
|
369
471
|
)
|
|
370
472
|
|
|
473
|
+
@override
|
|
371
474
|
def process(self, output):
|
|
372
475
|
return [flag for flag in output[0].split(",") if len(flag) > 0] if len(output) == 1 else []
|
|
373
476
|
|
|
@@ -403,6 +506,7 @@ class Block(FactBase):
|
|
|
403
506
|
# the list with a single empty string.
|
|
404
507
|
default = list
|
|
405
508
|
|
|
509
|
+
@override
|
|
406
510
|
def command(self, path, marker=None, begin=None, end=None):
|
|
407
511
|
self.path = path
|
|
408
512
|
start = (marker or MARKER_DEFAULT).format(mark=begin or MARKER_BEGIN_DEFAULT)
|
|
@@ -423,6 +527,7 @@ class Block(FactBase):
|
|
|
423
527
|
)
|
|
424
528
|
return cmd
|
|
425
529
|
|
|
530
|
+
@override
|
|
426
531
|
def process(self, output):
|
|
427
532
|
if output and (output[0] == f"{EXISTS}{self.path}"):
|
|
428
533
|
return []
|