pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinfra/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +18 -3
- pyinfra/api/arguments.py +406 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +67 -18
- pyinfra/api/facts.py +253 -202
- pyinfra/api/host.py +413 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +291 -368
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +381 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +670 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +410 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +630 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +746 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +98 -0
- pyinfra/operations/apt.py +488 -0
- pyinfra/operations/brew.py +231 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +191 -0
- pyinfra/operations/dnf.py +210 -0
- pyinfra/operations/docker.py +446 -0
- pyinfra/operations/files.py +1939 -0
- pyinfra/operations/flatpak.py +94 -0
- 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/gem.py +47 -0
- pyinfra/operations/git.py +419 -0
- pyinfra/operations/iptables.py +311 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +68 -0
- pyinfra/operations/mysql.py +609 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pacman.py +81 -0
- pyinfra/operations/pip.py +205 -0
- pyinfra/operations/pipx.py +102 -0
- pyinfra/operations/pkg.py +70 -0
- pyinfra/operations/pkgin.py +91 -0
- pyinfra/operations/postgres.py +436 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +40 -0
- pyinfra/operations/python.py +72 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +189 -0
- pyinfra/operations/server.py +1099 -0
- pyinfra/operations/snap.py +117 -0
- pyinfra/operations/ssh.py +216 -0
- pyinfra/operations/systemd.py +149 -0
- pyinfra/operations/sysvinit.py +141 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +251 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +336 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +77 -0
- pyinfra/operations/yum.py +210 -0
- pyinfra/operations/zfs.py +175 -0
- pyinfra/operations/zypper.py +192 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.5.1.dist-info/METADATA +141 -0
- pyinfra-3.5.1.dist-info/RECORD +159 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
- pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +780 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/facts/server.py
CHANGED
|
@@ -1,173 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
1
5
|
import re
|
|
6
|
+
import shutil
|
|
2
7
|
from datetime import datetime
|
|
8
|
+
from tempfile import mkdtemp
|
|
9
|
+
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
|
3
10
|
|
|
4
11
|
from dateutil.parser import parse as parse_date
|
|
12
|
+
from distro import distro
|
|
13
|
+
from typing_extensions import TypedDict, override
|
|
5
14
|
|
|
15
|
+
from pyinfra import host
|
|
6
16
|
from pyinfra.api import FactBase, ShortFactBase
|
|
7
17
|
from pyinfra.api.util import try_int
|
|
18
|
+
from pyinfra.facts import crontab
|
|
19
|
+
|
|
20
|
+
ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class User(FactBase):
|
|
24
|
+
"""
|
|
25
|
+
Returns the name of the current user.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
def command(self):
|
|
30
|
+
return "echo $USER"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Home(FactBase[Optional[str]]):
|
|
34
|
+
"""
|
|
35
|
+
Returns the home directory of the given user, or the current user if no user is given.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
def command(self, user=""):
|
|
40
|
+
return f"echo ~{user}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Path(FactBase):
|
|
44
|
+
"""
|
|
45
|
+
Returns the path environment variable of the current user.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
def command(self):
|
|
50
|
+
return "echo $PATH"
|
|
8
51
|
|
|
9
52
|
|
|
10
|
-
class
|
|
11
|
-
|
|
12
|
-
Returns the
|
|
13
|
-
|
|
53
|
+
class TmpDir(FactBase):
|
|
54
|
+
"""
|
|
55
|
+
Returns the temporary directory of the current server, if configured.
|
|
56
|
+
"""
|
|
14
57
|
|
|
15
|
-
|
|
58
|
+
@override
|
|
59
|
+
def command(self):
|
|
60
|
+
return "echo $TMPDIR"
|
|
16
61
|
|
|
17
62
|
|
|
18
63
|
class Hostname(FactBase):
|
|
19
|
-
|
|
64
|
+
"""
|
|
20
65
|
Returns the current hostname of the server.
|
|
21
|
-
|
|
66
|
+
"""
|
|
22
67
|
|
|
23
|
-
|
|
68
|
+
@override
|
|
69
|
+
def command(self):
|
|
70
|
+
return "uname -n"
|
|
24
71
|
|
|
25
72
|
|
|
26
|
-
class
|
|
27
|
-
|
|
73
|
+
class Kernel(FactBase):
|
|
74
|
+
"""
|
|
75
|
+
Returns the kernel name according to ``uname``.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@override
|
|
79
|
+
def command(self):
|
|
80
|
+
return "uname -s"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class KernelVersion(FactBase):
|
|
84
|
+
"""
|
|
85
|
+
Returns the kernel version according to ``uname``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
def command(self):
|
|
90
|
+
return "uname -r"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Deprecated/renamed -> Kernel
|
|
94
|
+
class Os(FactBase[str]):
|
|
95
|
+
"""
|
|
28
96
|
Returns the OS name according to ``uname``.
|
|
29
|
-
'''
|
|
30
97
|
|
|
31
|
-
|
|
98
|
+
.. warning::
|
|
99
|
+
This fact is deprecated/renamed, please use the ``server.Kernel`` fact.
|
|
100
|
+
"""
|
|
32
101
|
|
|
102
|
+
@override
|
|
103
|
+
def command(self):
|
|
104
|
+
return "uname -s"
|
|
33
105
|
|
|
34
|
-
|
|
35
|
-
|
|
106
|
+
|
|
107
|
+
# Deprecated/renamed -> KernelVersion
|
|
108
|
+
class OsVersion(FactBase[str]):
|
|
109
|
+
"""
|
|
36
110
|
Returns the OS version according to ``uname``.
|
|
37
|
-
'''
|
|
38
111
|
|
|
39
|
-
|
|
112
|
+
.. warning::
|
|
113
|
+
This fact is deprecated/renamed, please use the ``server.KernelVersion`` fact.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
def command(self):
|
|
118
|
+
return "uname -r"
|
|
40
119
|
|
|
41
120
|
|
|
42
|
-
class Arch(FactBase):
|
|
43
|
-
|
|
121
|
+
class Arch(FactBase[str]):
|
|
122
|
+
"""
|
|
44
123
|
Returns the system architecture according to ``uname``.
|
|
45
|
-
|
|
124
|
+
"""
|
|
46
125
|
|
|
47
|
-
|
|
126
|
+
# ``uname -p`` is not portable and returns ``unknown`` on Debian.
|
|
127
|
+
# ``uname -m`` works on most Linux and BSD systems.
|
|
128
|
+
@override
|
|
129
|
+
def command(self):
|
|
130
|
+
return "uname -m"
|
|
48
131
|
|
|
49
132
|
|
|
50
|
-
class Command(FactBase):
|
|
51
|
-
|
|
133
|
+
class Command(FactBase[str]):
|
|
134
|
+
"""
|
|
52
135
|
Returns the raw output lines of a given command.
|
|
53
|
-
|
|
136
|
+
"""
|
|
54
137
|
|
|
55
|
-
@
|
|
56
|
-
def command(command):
|
|
138
|
+
@override
|
|
139
|
+
def command(self, command):
|
|
57
140
|
return command
|
|
58
141
|
|
|
59
142
|
|
|
60
|
-
class Which(FactBase):
|
|
61
|
-
|
|
62
|
-
Returns the path of a given command
|
|
63
|
-
|
|
143
|
+
class Which(FactBase[Optional[str]]):
|
|
144
|
+
"""
|
|
145
|
+
Returns the path of a given command according to `command -v`, if available.
|
|
146
|
+
"""
|
|
64
147
|
|
|
65
|
-
@
|
|
66
|
-
def command(
|
|
67
|
-
return
|
|
148
|
+
@override
|
|
149
|
+
def command(self, command):
|
|
150
|
+
return "command -v {0} || true".format(command)
|
|
68
151
|
|
|
69
152
|
|
|
70
|
-
class Date(FactBase):
|
|
71
|
-
|
|
153
|
+
class Date(FactBase[datetime]):
|
|
154
|
+
"""
|
|
72
155
|
Returns the current datetime on the server.
|
|
73
|
-
|
|
156
|
+
"""
|
|
74
157
|
|
|
75
|
-
command = 'LANG=C date'
|
|
76
158
|
default = datetime.now
|
|
77
159
|
|
|
78
|
-
@
|
|
79
|
-
def
|
|
80
|
-
return
|
|
160
|
+
@override
|
|
161
|
+
def command(self):
|
|
162
|
+
return f"date +'{ISO_DATE_FORMAT}'"
|
|
81
163
|
|
|
164
|
+
@override
|
|
165
|
+
def process(self, output) -> datetime:
|
|
166
|
+
return datetime.strptime(list(output)[0], ISO_DATE_FORMAT)
|
|
82
167
|
|
|
83
|
-
|
|
84
|
-
|
|
168
|
+
|
|
169
|
+
class MacosVersion(FactBase[str]):
|
|
170
|
+
"""
|
|
171
|
+
Returns the installed MacOS version.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
@override
|
|
175
|
+
def requires_command(self) -> str:
|
|
176
|
+
return "sw_vers"
|
|
177
|
+
|
|
178
|
+
@override
|
|
179
|
+
def command(self):
|
|
180
|
+
return "sw_vers -productVersion"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class MountsDict(TypedDict):
|
|
184
|
+
device: str
|
|
185
|
+
type: str
|
|
186
|
+
options: list[str]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Mounts(FactBase[Dict[str, MountsDict]]):
|
|
190
|
+
"""
|
|
85
191
|
Returns a dictionary of mounted filesystems and information.
|
|
86
192
|
|
|
87
193
|
.. code:: python
|
|
88
194
|
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
195
|
+
{
|
|
196
|
+
"/": {
|
|
197
|
+
"device": "/dev/mv2",
|
|
198
|
+
"type": "ext4",
|
|
199
|
+
"options": [
|
|
200
|
+
"rw",
|
|
201
|
+
"relatime"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
"""
|
|
99
206
|
|
|
100
|
-
command = 'mount'
|
|
101
207
|
default = dict
|
|
102
208
|
|
|
103
|
-
@
|
|
104
|
-
def
|
|
105
|
-
|
|
209
|
+
@override
|
|
210
|
+
def command(self) -> str:
|
|
211
|
+
self._kernel = host.get_fact(Kernel)
|
|
106
212
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
is_map = True
|
|
213
|
+
if self._kernel.strip() == "FreeBSD":
|
|
214
|
+
return "mount -p --libxo json"
|
|
215
|
+
else:
|
|
216
|
+
return "cat /proc/self/mountinfo"
|
|
112
217
|
|
|
113
|
-
|
|
218
|
+
@override
|
|
219
|
+
def process(self, output) -> dict[str, MountsDict]:
|
|
220
|
+
devices: dict[str, MountsDict] = {}
|
|
114
221
|
|
|
115
|
-
|
|
116
|
-
|
|
222
|
+
def unescape_octal(match: re.Match) -> str:
|
|
223
|
+
s = match.group(0)[1:] # skip the backslash
|
|
224
|
+
return chr(int(s, base=8))
|
|
117
225
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
226
|
+
def replace_octal(s: str) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Unescape strings encoded by linux's string_escape_mem with ESCAPE_OCTAL flag.
|
|
229
|
+
"""
|
|
230
|
+
return re.sub(r"\\[0-7]{3}", unescape_octal, s)
|
|
231
|
+
|
|
232
|
+
if self._kernel == "FreeBSD":
|
|
233
|
+
full_output = "\n".join(output)
|
|
234
|
+
json_output = json.loads(full_output)
|
|
235
|
+
mount_fstab = json_output["mount"]["fstab"]
|
|
236
|
+
|
|
237
|
+
for entry in mount_fstab:
|
|
238
|
+
path = entry["mntpoint"]
|
|
239
|
+
type_ = entry["fstype"]
|
|
240
|
+
device = entry["device"]
|
|
241
|
+
options = [option.strip() for option in entry["opts"].split(",")]
|
|
242
|
+
|
|
243
|
+
devices[path] = {"device": device, "type": type_, "options": options}
|
|
244
|
+
|
|
245
|
+
return devices
|
|
246
|
+
|
|
247
|
+
for line in output:
|
|
248
|
+
# ignore mount ID, parent ID, major:minor, root
|
|
249
|
+
_, _, _, _, mount_point, mount_options, line = line.split(sep=" ", maxsplit=6)
|
|
250
|
+
|
|
251
|
+
# ignore optional tags "shared", "master", "propagate_from" and "unbindable"
|
|
252
|
+
while True:
|
|
253
|
+
optional, line = line.split(sep=" ", maxsplit=1)
|
|
254
|
+
if optional == "-":
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
fs_type, mount_source, super_options = line.split(sep=" ")
|
|
258
|
+
|
|
259
|
+
mount_options = mount_options.split(sep=",")
|
|
260
|
+
|
|
261
|
+
# escaped: mount_point, mount_source, super_options
|
|
262
|
+
# these strings can contain characters encoded in octal, e.g. '\054' for ','
|
|
263
|
+
mount_point = replace_octal(mount_point)
|
|
264
|
+
mount_source = replace_octal(mount_source)
|
|
265
|
+
|
|
266
|
+
# mount_options will override ro/rw and can be different than the super block options
|
|
267
|
+
# filter them, so they don't appear twice
|
|
268
|
+
super_options = [
|
|
269
|
+
replace_octal(opt)
|
|
270
|
+
for opt in super_options.split(sep=",")
|
|
271
|
+
if opt not in ["ro", "rw"]
|
|
272
|
+
]
|
|
124
273
|
|
|
125
|
-
devices[
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
274
|
+
devices[mount_point] = {
|
|
275
|
+
"device": mount_source,
|
|
276
|
+
"type": fs_type,
|
|
277
|
+
"options": mount_options + super_options,
|
|
129
278
|
}
|
|
130
279
|
|
|
131
280
|
return devices
|
|
132
281
|
|
|
133
282
|
|
|
283
|
+
class Port(FactBase[Union[Tuple[str, int], Tuple[None, None]]]):
|
|
284
|
+
"""
|
|
285
|
+
Returns the process occuping a port and its PID
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
@override
|
|
289
|
+
def command(self, port: int) -> str:
|
|
290
|
+
return f"ss -lptnH 'src :{port}'"
|
|
291
|
+
|
|
292
|
+
@override
|
|
293
|
+
def process(self, output: Iterable[str]) -> Union[Tuple[str, int], Tuple[None, None]]:
|
|
294
|
+
for line in output:
|
|
295
|
+
proc, pid = line.split('"')[1], int(line.split("pid=")[1].split(",")[0])
|
|
296
|
+
return (proc, pid)
|
|
297
|
+
return None, None
|
|
298
|
+
|
|
299
|
+
|
|
134
300
|
class KernelModules(FactBase):
|
|
135
|
-
|
|
301
|
+
"""
|
|
136
302
|
Returns a dictionary of kernel module name -> info.
|
|
137
303
|
|
|
138
304
|
.. code:: python
|
|
139
305
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
306
|
+
{
|
|
307
|
+
"module_name": {
|
|
308
|
+
"size": 0,
|
|
309
|
+
"instances": 0,
|
|
310
|
+
"state": "Live",
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
@override
|
|
316
|
+
def command(self):
|
|
317
|
+
return "! test -f /proc/modules || cat /proc/modules"
|
|
147
318
|
|
|
148
|
-
command = 'cat /proc/modules'
|
|
149
319
|
default = dict
|
|
150
320
|
|
|
151
|
-
@
|
|
152
|
-
def process(output):
|
|
321
|
+
@override
|
|
322
|
+
def process(self, output):
|
|
153
323
|
modules = {}
|
|
154
324
|
|
|
155
325
|
for line in output:
|
|
156
|
-
name, size, instances, depends, state, _ = line.split(
|
|
326
|
+
name, size, instances, depends, state, _ = line.split(" ", 5)
|
|
157
327
|
instances = int(instances)
|
|
158
328
|
|
|
159
329
|
module = {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
330
|
+
"size": size,
|
|
331
|
+
"instances": instances,
|
|
332
|
+
"state": state,
|
|
163
333
|
}
|
|
164
334
|
|
|
165
|
-
if depends !=
|
|
166
|
-
module[
|
|
167
|
-
value
|
|
168
|
-
for value in depends.split(',')
|
|
169
|
-
if value
|
|
170
|
-
]
|
|
335
|
+
if depends != "-":
|
|
336
|
+
module["depends"] = [value for value in depends.split(",") if value]
|
|
171
337
|
|
|
172
338
|
modules[name] = module
|
|
173
339
|
|
|
@@ -175,7 +341,7 @@ class KernelModules(FactBase):
|
|
|
175
341
|
|
|
176
342
|
|
|
177
343
|
class LsbRelease(FactBase):
|
|
178
|
-
|
|
344
|
+
"""
|
|
179
345
|
Returns a dictionary of release information using ``lsb_release``.
|
|
180
346
|
|
|
181
347
|
.. code:: python
|
|
@@ -187,25 +353,31 @@ class LsbRelease(FactBase):
|
|
|
187
353
|
"codename": "bionic",
|
|
188
354
|
...
|
|
189
355
|
}
|
|
190
|
-
|
|
356
|
+
"""
|
|
191
357
|
|
|
192
|
-
|
|
358
|
+
@override
|
|
359
|
+
def command(self):
|
|
360
|
+
return "lsb_release -ca"
|
|
193
361
|
|
|
194
|
-
@
|
|
195
|
-
def
|
|
362
|
+
@override
|
|
363
|
+
def requires_command(self):
|
|
364
|
+
return "lsb_release"
|
|
365
|
+
|
|
366
|
+
@override
|
|
367
|
+
def process(self, output):
|
|
196
368
|
items = {}
|
|
197
369
|
|
|
198
370
|
for line in output:
|
|
199
|
-
if
|
|
371
|
+
if ":" not in line:
|
|
200
372
|
continue
|
|
201
373
|
|
|
202
|
-
key, value = line.split(
|
|
374
|
+
key, value = line.split(":", 1)
|
|
203
375
|
|
|
204
376
|
key = key.strip().lower()
|
|
205
377
|
|
|
206
378
|
# Turn "distributor id" into "id"
|
|
207
|
-
if
|
|
208
|
-
key = key.split(
|
|
379
|
+
if " " in key:
|
|
380
|
+
key = key.split(" ")[-1]
|
|
209
381
|
|
|
210
382
|
value = value.strip()
|
|
211
383
|
|
|
@@ -214,8 +386,40 @@ class LsbRelease(FactBase):
|
|
|
214
386
|
return items
|
|
215
387
|
|
|
216
388
|
|
|
389
|
+
class OsRelease(FactBase):
|
|
390
|
+
"""
|
|
391
|
+
Returns a dictionary of release information stored in ``/etc/os-release``.
|
|
392
|
+
|
|
393
|
+
.. code:: python
|
|
394
|
+
|
|
395
|
+
{
|
|
396
|
+
"name": "EndeavourOS",
|
|
397
|
+
"pretty_name": "EndeavourOS",
|
|
398
|
+
"id": "endeavouros",
|
|
399
|
+
"id_like": "arch",
|
|
400
|
+
"build_id": "2024.06.25",
|
|
401
|
+
...
|
|
402
|
+
}
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
@override
|
|
406
|
+
def command(self):
|
|
407
|
+
return "cat /etc/os-release"
|
|
408
|
+
|
|
409
|
+
@override
|
|
410
|
+
def process(self, output):
|
|
411
|
+
items = {}
|
|
412
|
+
|
|
413
|
+
for line in output:
|
|
414
|
+
if "=" in line:
|
|
415
|
+
key, value = line.split("=", 1)
|
|
416
|
+
items[key.strip().lower()] = value.strip().strip('"')
|
|
417
|
+
|
|
418
|
+
return items
|
|
419
|
+
|
|
420
|
+
|
|
217
421
|
class Sysctl(FactBase):
|
|
218
|
-
|
|
422
|
+
"""
|
|
219
423
|
Returns a dictionary of sysctl settings and values.
|
|
220
424
|
|
|
221
425
|
.. code:: python
|
|
@@ -226,24 +430,28 @@ class Sysctl(FactBase):
|
|
|
226
430
|
44565,
|
|
227
431
|
360,
|
|
228
432
|
],
|
|
229
|
-
...
|
|
230
433
|
}
|
|
231
|
-
|
|
434
|
+
"""
|
|
232
435
|
|
|
233
|
-
command = 'sysctl -a'
|
|
234
436
|
default = dict
|
|
235
437
|
|
|
236
|
-
@
|
|
237
|
-
def
|
|
438
|
+
@override
|
|
439
|
+
def command(self, keys=None):
|
|
440
|
+
if keys is None:
|
|
441
|
+
return "sysctl -a"
|
|
442
|
+
return f"sysctl {' '.join(keys)}"
|
|
443
|
+
|
|
444
|
+
@override
|
|
445
|
+
def process(self, output):
|
|
238
446
|
sysctls = {}
|
|
239
447
|
|
|
240
448
|
for line in output:
|
|
241
449
|
key = values = None
|
|
242
450
|
|
|
243
|
-
if
|
|
244
|
-
key, values = line.split(
|
|
245
|
-
elif
|
|
246
|
-
key, values = line.split(
|
|
451
|
+
if "=" in line:
|
|
452
|
+
key, values = line.split("=", 1)
|
|
453
|
+
elif ":" in line:
|
|
454
|
+
key, values = line.split(":", 1)
|
|
247
455
|
else:
|
|
248
456
|
continue # pragma: no cover
|
|
249
457
|
|
|
@@ -251,11 +459,8 @@ class Sysctl(FactBase):
|
|
|
251
459
|
key = key.strip()
|
|
252
460
|
values = values.strip()
|
|
253
461
|
|
|
254
|
-
if re.match(r
|
|
255
|
-
values = [
|
|
256
|
-
try_int(item.strip())
|
|
257
|
-
for item in values.split()
|
|
258
|
-
]
|
|
462
|
+
if re.match(r"^[a-zA-Z0-9_\-\.\s]+$", values):
|
|
463
|
+
values = [try_int(item.strip()) for item in values.split()]
|
|
259
464
|
|
|
260
465
|
if len(values) == 1:
|
|
261
466
|
values = values[0]
|
|
@@ -265,156 +470,125 @@ class Sysctl(FactBase):
|
|
|
265
470
|
return sysctls
|
|
266
471
|
|
|
267
472
|
|
|
268
|
-
class Groups(FactBase):
|
|
269
|
-
|
|
473
|
+
class Groups(FactBase[List[str]]):
|
|
474
|
+
"""
|
|
270
475
|
Returns a list of groups on the system.
|
|
271
|
-
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
@override
|
|
479
|
+
def command(self):
|
|
480
|
+
return "cat /etc/group"
|
|
272
481
|
|
|
273
|
-
command = 'cat /etc/group'
|
|
274
482
|
default = list
|
|
275
483
|
|
|
276
|
-
@
|
|
277
|
-
def process(output):
|
|
278
|
-
groups = []
|
|
484
|
+
@override
|
|
485
|
+
def process(self, output) -> list[str]:
|
|
486
|
+
groups: list[str] = []
|
|
279
487
|
|
|
280
488
|
for line in output:
|
|
281
|
-
if
|
|
282
|
-
groups.append(line.split(
|
|
489
|
+
if ":" in line:
|
|
490
|
+
groups.append(line.split(":")[0])
|
|
283
491
|
|
|
284
492
|
return groups
|
|
285
493
|
|
|
286
494
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.. code:: python
|
|
292
|
-
|
|
293
|
-
'/path/to/command': {
|
|
294
|
-
'minute': '*',
|
|
295
|
-
'hour': '*',
|
|
296
|
-
'month': '*',
|
|
297
|
-
'day_of_month': '*',
|
|
298
|
-
'day_of_week': '*',
|
|
299
|
-
},
|
|
300
|
-
...
|
|
301
|
-
'''
|
|
302
|
-
|
|
303
|
-
default = dict
|
|
304
|
-
|
|
305
|
-
@staticmethod
|
|
306
|
-
def command(user=None):
|
|
307
|
-
if user:
|
|
308
|
-
return 'crontab -l -u {0}'.format(user)
|
|
309
|
-
|
|
310
|
-
return 'crontab -l'
|
|
311
|
-
|
|
312
|
-
@staticmethod
|
|
313
|
-
def process(output):
|
|
314
|
-
crons = {}
|
|
315
|
-
current_comments = []
|
|
316
|
-
|
|
317
|
-
for line in output:
|
|
318
|
-
line = line.strip()
|
|
319
|
-
if not line or line.startswith('#'):
|
|
320
|
-
current_comments.append(line)
|
|
321
|
-
continue
|
|
322
|
-
|
|
323
|
-
minute, hour, day_of_month, month, day_of_week, command = line.split(' ', 5)
|
|
324
|
-
crons[command] = {
|
|
325
|
-
'minute': try_int(minute),
|
|
326
|
-
'hour': try_int(hour),
|
|
327
|
-
'month': try_int(month),
|
|
328
|
-
'day_of_month': try_int(day_of_month),
|
|
329
|
-
'day_of_week': try_int(day_of_week),
|
|
330
|
-
'comments': current_comments,
|
|
331
|
-
}
|
|
332
|
-
current_comments = []
|
|
333
|
-
|
|
334
|
-
return crons
|
|
495
|
+
# for compatibility
|
|
496
|
+
CrontabDict = crontab.CrontabDict
|
|
497
|
+
Crontab = crontab.Crontab
|
|
335
498
|
|
|
336
499
|
|
|
337
500
|
class Users(FactBase):
|
|
338
|
-
|
|
501
|
+
"""
|
|
339
502
|
Returns a dictionary of users -> details.
|
|
340
503
|
|
|
341
504
|
.. code:: python
|
|
342
505
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
506
|
+
{
|
|
507
|
+
"user_name": {
|
|
508
|
+
"comment": "Full Name",
|
|
509
|
+
"home": "/home/user_name",
|
|
510
|
+
"shell": "/bin/bash,
|
|
511
|
+
"group": "main_user_group",
|
|
512
|
+
"groups": [
|
|
513
|
+
"other",
|
|
514
|
+
"groups"
|
|
515
|
+
],
|
|
516
|
+
"uid": user_id,
|
|
517
|
+
"gid": main_user_group_id,
|
|
518
|
+
"lastlog": last_login_time,
|
|
519
|
+
"password": encrypted_password,
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
"""
|
|
523
|
+
|
|
524
|
+
@override
|
|
525
|
+
def command(self):
|
|
526
|
+
return """
|
|
354
527
|
|
|
355
|
-
command = '''
|
|
356
528
|
for i in `cat /etc/passwd | cut -d: -f1`; do
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
529
|
+
ENTRY=`grep ^$i: /etc/passwd`;
|
|
530
|
+
LASTLOG=`(((lastlog -u $i || lastlogin $i) 2> /dev/null) | grep ^$i | tr -s ' ')`;
|
|
531
|
+
PASSWORD=`(grep ^$i: /etc/shadow || grep ^$i: /etc/master.passwd) 2> /dev/null | cut -d: -f2`;
|
|
532
|
+
echo "$ENTRY|`id -gn $i`|`id -Gn $i`|$LASTLOG|$PASSWORD";
|
|
360
533
|
done
|
|
361
|
-
|
|
534
|
+
""".strip() # noqa
|
|
362
535
|
|
|
363
536
|
default = dict
|
|
364
537
|
|
|
365
|
-
|
|
366
|
-
group_regex = r'^[0-9]+\(([a-zA-Z0-9_\.\-]+)\)$'
|
|
367
|
-
|
|
538
|
+
@override
|
|
368
539
|
def process(self, output):
|
|
369
540
|
users = {}
|
|
370
|
-
|
|
371
|
-
matches = re.match(self.regex, line)
|
|
372
|
-
|
|
373
|
-
if matches:
|
|
374
|
-
# Parse out the home/shell
|
|
375
|
-
home_shell = matches.group(4)
|
|
376
|
-
home = shell = None
|
|
377
|
-
|
|
378
|
-
# /blah: is just a home
|
|
379
|
-
if home_shell.endswith(':'):
|
|
380
|
-
home = home_shell[:-1]
|
|
381
|
-
|
|
382
|
-
# :/blah is just a shell
|
|
383
|
-
elif home_shell.startswith(':'):
|
|
384
|
-
shell = home_shell[1:]
|
|
541
|
+
rex = r"[A-Z][a-z]{2} [A-Z][a-z]{2} {1,2}\d+ .+$"
|
|
385
542
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
home, shell = home_shell.split(':')
|
|
543
|
+
for line in output:
|
|
544
|
+
entry, group, user_groups, lastlog, password = line.rsplit("|", 4)
|
|
389
545
|
|
|
390
|
-
|
|
391
|
-
|
|
546
|
+
if entry:
|
|
547
|
+
# Parse out the comment/home/shell
|
|
548
|
+
entries = entry.split(":")
|
|
392
549
|
|
|
393
|
-
# Parse
|
|
550
|
+
# Parse groups
|
|
394
551
|
groups = []
|
|
395
|
-
for
|
|
396
|
-
name = re.match(self.group_regex, group_matches.strip())
|
|
397
|
-
if name:
|
|
398
|
-
name = name.group(1)
|
|
399
|
-
else:
|
|
400
|
-
continue # pragma: no cover
|
|
401
|
-
|
|
552
|
+
for group_name in user_groups.split(" "):
|
|
402
553
|
# We only want secondary groups here
|
|
403
|
-
if
|
|
404
|
-
groups.append(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
554
|
+
if group_name and group_name != group:
|
|
555
|
+
groups.append(group_name)
|
|
556
|
+
|
|
557
|
+
raw_login_time = None
|
|
558
|
+
login_time = None
|
|
559
|
+
|
|
560
|
+
# Parse lastlog info
|
|
561
|
+
# lastlog output varies, which is why I use regex to match login time
|
|
562
|
+
login = re.search(rex, lastlog)
|
|
563
|
+
if login:
|
|
564
|
+
raw_login_time = login.group()
|
|
565
|
+
login_time = parse_date(raw_login_time)
|
|
566
|
+
|
|
567
|
+
users[entries[0]] = {
|
|
568
|
+
"home": entries[5] or None,
|
|
569
|
+
"comment": entries[4] or None,
|
|
570
|
+
"shell": entries[6] or None,
|
|
571
|
+
"group": group,
|
|
572
|
+
"groups": groups,
|
|
573
|
+
"uid": int(entries[2]),
|
|
574
|
+
"gid": int(entries[3]),
|
|
575
|
+
"lastlog": raw_login_time,
|
|
576
|
+
"login_time": login_time,
|
|
577
|
+
"password": password,
|
|
411
578
|
}
|
|
412
579
|
|
|
413
580
|
return users
|
|
414
581
|
|
|
415
582
|
|
|
416
|
-
class
|
|
417
|
-
|
|
583
|
+
class LinuxDistributionDict(TypedDict):
|
|
584
|
+
name: Optional[str]
|
|
585
|
+
major: Optional[int]
|
|
586
|
+
minor: Optional[int]
|
|
587
|
+
release_meta: Dict
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class LinuxDistribution(FactBase[LinuxDistributionDict]):
|
|
591
|
+
"""
|
|
418
592
|
Returns a dict of the Linux distribution version. Ubuntu, Debian, CentOS,
|
|
419
593
|
Fedora & Gentoo currently. Also contains any key/value items located in
|
|
420
594
|
release files.
|
|
@@ -422,72 +596,359 @@ class LinuxDistribution(FactBase):
|
|
|
422
596
|
.. code:: python
|
|
423
597
|
|
|
424
598
|
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
599
|
+
"name": "Ubuntu",
|
|
600
|
+
"major": 20,
|
|
601
|
+
"minor": 04,
|
|
602
|
+
"release_meta": {
|
|
603
|
+
"CODENAME": "focal",
|
|
604
|
+
"ID_LIKE": "debian",
|
|
430
605
|
...
|
|
431
606
|
}
|
|
432
607
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
@override
|
|
611
|
+
def command(self) -> str:
|
|
612
|
+
return (
|
|
613
|
+
"cd /etc/ && for file in $(ls -pdL *-release | grep -v /); "
|
|
614
|
+
'do echo "/etc/${file}"; cat "/etc/${file}"; echo ---; '
|
|
615
|
+
"done"
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
name_to_pretty_name = {
|
|
619
|
+
"alpine": "Alpine",
|
|
620
|
+
"centos": "CentOS",
|
|
621
|
+
"fedora": "Fedora",
|
|
622
|
+
"gentoo": "Gentoo",
|
|
623
|
+
"opensuse": "openSUSE",
|
|
624
|
+
"rhel": "RedHat",
|
|
625
|
+
"ubuntu": "Ubuntu",
|
|
626
|
+
"debian": "Debian",
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
@override
|
|
449
630
|
@staticmethod
|
|
450
|
-
def default():
|
|
631
|
+
def default() -> LinuxDistributionDict:
|
|
451
632
|
return {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
633
|
+
"name": None,
|
|
634
|
+
"major": None,
|
|
635
|
+
"minor": None,
|
|
636
|
+
"release_meta": {},
|
|
455
637
|
}
|
|
456
638
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
639
|
+
@override
|
|
640
|
+
def process(self, output) -> LinuxDistributionDict:
|
|
641
|
+
parts = {}
|
|
642
|
+
for part in "\n".join(output).strip().split("---"):
|
|
643
|
+
if not part.strip():
|
|
644
|
+
continue
|
|
645
|
+
try:
|
|
646
|
+
filename, content = part.strip().split("\n", 1)
|
|
647
|
+
parts[filename] = content
|
|
648
|
+
except ValueError:
|
|
649
|
+
# skip empty files
|
|
650
|
+
# for instance arch linux as an empty file at /etc/arch-release
|
|
651
|
+
continue
|
|
464
652
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
653
|
+
release_info = self.default()
|
|
654
|
+
if not parts:
|
|
655
|
+
return release_info
|
|
656
|
+
|
|
657
|
+
temp_root = mkdtemp()
|
|
658
|
+
try:
|
|
659
|
+
temp_etc_dir = os.path.join(temp_root, "etc")
|
|
660
|
+
os.mkdir(temp_etc_dir)
|
|
661
|
+
|
|
662
|
+
for filename, content in parts.items():
|
|
663
|
+
with open(
|
|
664
|
+
os.path.join(temp_etc_dir, os.path.basename(filename)),
|
|
665
|
+
"w",
|
|
666
|
+
encoding="utf-8",
|
|
667
|
+
) as fp:
|
|
668
|
+
fp.write(content)
|
|
669
|
+
|
|
670
|
+
parsed = distro.LinuxDistribution(
|
|
671
|
+
root_dir=temp_root,
|
|
672
|
+
include_lsb=False,
|
|
673
|
+
include_uname=False,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
release_meta = {key.upper(): value for key, value in parsed.os_release_info().items()}
|
|
677
|
+
# Distro 1.7+ adds this, breaking tests
|
|
678
|
+
# TODO: fix this!
|
|
679
|
+
release_meta.pop("RELEASE_CODENAME", None)
|
|
680
|
+
|
|
681
|
+
release_info.update(
|
|
682
|
+
{
|
|
683
|
+
"name": self.name_to_pretty_name.get(parsed.id(), parsed.name()),
|
|
684
|
+
"major": try_int(parsed.major_version()) or None,
|
|
685
|
+
"minor": try_int(parsed.minor_version()) or None,
|
|
686
|
+
"release_meta": release_meta,
|
|
687
|
+
},
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
finally:
|
|
691
|
+
shutil.rmtree(temp_root)
|
|
479
692
|
|
|
480
693
|
return release_info
|
|
481
694
|
|
|
482
695
|
|
|
483
|
-
class LinuxName(ShortFactBase):
|
|
484
|
-
|
|
696
|
+
class LinuxName(ShortFactBase[str]):
|
|
697
|
+
"""
|
|
485
698
|
Returns the name of the Linux distribution. Shortcut for
|
|
486
|
-
``host.
|
|
487
|
-
|
|
699
|
+
``host.get_fact(LinuxDistribution)['name']``.
|
|
700
|
+
"""
|
|
488
701
|
|
|
489
702
|
fact = LinuxDistribution
|
|
490
703
|
|
|
704
|
+
@override
|
|
705
|
+
def process_data(self, data) -> str:
|
|
706
|
+
return data["name"]
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class SelinuxDict(TypedDict):
|
|
710
|
+
mode: Optional[str]
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class Selinux(FactBase[SelinuxDict]):
|
|
714
|
+
"""
|
|
715
|
+
Discovers the SELinux related facts on the target host.
|
|
716
|
+
|
|
717
|
+
.. code:: python
|
|
718
|
+
|
|
719
|
+
{
|
|
720
|
+
"mode": "enabled",
|
|
721
|
+
}
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
@override
|
|
725
|
+
def command(self):
|
|
726
|
+
return "sestatus"
|
|
727
|
+
|
|
728
|
+
@override
|
|
729
|
+
def requires_command(self) -> str:
|
|
730
|
+
return "sestatus"
|
|
731
|
+
|
|
732
|
+
@override
|
|
491
733
|
@staticmethod
|
|
492
|
-
def
|
|
493
|
-
return
|
|
734
|
+
def default() -> SelinuxDict:
|
|
735
|
+
return {
|
|
736
|
+
"mode": None,
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
@override
|
|
740
|
+
def process(self, output) -> SelinuxDict:
|
|
741
|
+
selinux_info = self.default()
|
|
742
|
+
|
|
743
|
+
match = re.match(r"^SELinux status:\s+(\S+)", "\n".join(output))
|
|
744
|
+
|
|
745
|
+
if not match:
|
|
746
|
+
return selinux_info
|
|
747
|
+
|
|
748
|
+
selinux_info["mode"] = match.group(1)
|
|
749
|
+
|
|
750
|
+
return selinux_info
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class LinuxGui(FactBase[List[str]]):
|
|
754
|
+
"""
|
|
755
|
+
Returns a list of available Linux GUIs.
|
|
756
|
+
"""
|
|
757
|
+
|
|
758
|
+
@override
|
|
759
|
+
def command(self):
|
|
760
|
+
return "ls /usr/bin/*session || true"
|
|
761
|
+
|
|
762
|
+
default = list
|
|
763
|
+
|
|
764
|
+
known_gui_binaries = {
|
|
765
|
+
"/usr/bin/gnome-session": "GNOME",
|
|
766
|
+
"/usr/bin/mate-session": "MATE",
|
|
767
|
+
"/usr/bin/lxsession": "LXDE",
|
|
768
|
+
"/usr/bin/plasma_session": "KDE Plasma",
|
|
769
|
+
"/usr/bin/xfce4-session": "XFCE 4",
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
@override
|
|
773
|
+
def process(self, output) -> list[str]:
|
|
774
|
+
gui_names = []
|
|
775
|
+
|
|
776
|
+
for line in output:
|
|
777
|
+
gui_name = self.known_gui_binaries.get(line)
|
|
778
|
+
if gui_name:
|
|
779
|
+
gui_names.append(gui_name)
|
|
780
|
+
|
|
781
|
+
return gui_names
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
class HasGui(ShortFactBase[bool]):
|
|
785
|
+
"""
|
|
786
|
+
Returns a boolean indicating the remote side has GUI capabilities. Linux only.
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
fact = LinuxGui
|
|
790
|
+
|
|
791
|
+
@override
|
|
792
|
+
def process_data(self, data) -> bool:
|
|
793
|
+
return len(data) > 0
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
class Locales(FactBase[List[str]]):
|
|
797
|
+
"""
|
|
798
|
+
Returns installed locales on the target host.
|
|
799
|
+
|
|
800
|
+
.. code:: python
|
|
801
|
+
|
|
802
|
+
["C.UTF-8", "en_US.UTF-8"]
|
|
803
|
+
"""
|
|
804
|
+
|
|
805
|
+
@override
|
|
806
|
+
def command(self) -> str:
|
|
807
|
+
return "locale -a"
|
|
808
|
+
|
|
809
|
+
@override
|
|
810
|
+
def requires_command(self) -> str:
|
|
811
|
+
return "locale"
|
|
812
|
+
|
|
813
|
+
default = list
|
|
814
|
+
|
|
815
|
+
@override
|
|
816
|
+
def process(self, output) -> list[str]:
|
|
817
|
+
# replace utf8 with UTF-8 to match names in /etc/locale.gen
|
|
818
|
+
# return a list of enabled locales
|
|
819
|
+
return [line.replace("utf8", "UTF-8") for line in output]
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
class SecurityLimits(FactBase):
|
|
823
|
+
"""
|
|
824
|
+
Returns a list of security limits on the target host.
|
|
825
|
+
|
|
826
|
+
.. code:: python
|
|
827
|
+
|
|
828
|
+
[
|
|
829
|
+
{
|
|
830
|
+
"domain": "*",
|
|
831
|
+
"limit_type": "soft",
|
|
832
|
+
"item": "nofile",
|
|
833
|
+
"value": "1048576"
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
"domain": "*",
|
|
837
|
+
"limit_type": "hard",
|
|
838
|
+
"item": "nofile",
|
|
839
|
+
"value": "1048576"
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"domain": "root",
|
|
843
|
+
"limit_type": "soft",
|
|
844
|
+
"item": "nofile",
|
|
845
|
+
"value": "1048576"
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"domain": "root",
|
|
849
|
+
"limit_type": "hard",
|
|
850
|
+
"item": "nofile",
|
|
851
|
+
"value": "1048576"
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
"domain": "*",
|
|
855
|
+
"limit_type": "soft",
|
|
856
|
+
"item": "memlock",
|
|
857
|
+
"value": "unlimited"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"domain": "*",
|
|
861
|
+
"limit_type": "hard",
|
|
862
|
+
"item": "memlock",
|
|
863
|
+
"value": "unlimited"
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
"domain": "root",
|
|
867
|
+
"limit_type": "soft",
|
|
868
|
+
"item": "memlock",
|
|
869
|
+
"value": "unlimited"
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
"domain": "root",
|
|
873
|
+
"limit_type": "hard",
|
|
874
|
+
"item": "memlock",
|
|
875
|
+
"value": "unlimited"
|
|
876
|
+
}
|
|
877
|
+
]
|
|
878
|
+
"""
|
|
879
|
+
|
|
880
|
+
@override
|
|
881
|
+
def command(self):
|
|
882
|
+
return "cat /etc/security/limits.conf"
|
|
883
|
+
|
|
884
|
+
default = list
|
|
885
|
+
|
|
886
|
+
@override
|
|
887
|
+
def process(self, output):
|
|
888
|
+
limits = []
|
|
889
|
+
|
|
890
|
+
for line in output:
|
|
891
|
+
if line.startswith("#") or not len(line.strip()):
|
|
892
|
+
continue
|
|
893
|
+
|
|
894
|
+
domain, limit_type, item, value = line.split()
|
|
895
|
+
|
|
896
|
+
limits.append(
|
|
897
|
+
{
|
|
898
|
+
"domain": domain,
|
|
899
|
+
"limit_type": limit_type,
|
|
900
|
+
"item": item,
|
|
901
|
+
"value": value,
|
|
902
|
+
},
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
return limits
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
class RebootRequired(FactBase[bool]):
|
|
909
|
+
"""
|
|
910
|
+
Returns a boolean indicating whether the system requires a reboot.
|
|
911
|
+
|
|
912
|
+
On Linux systems:
|
|
913
|
+
- Checks /var/run/reboot-required and /var/run/reboot-required.pkgs
|
|
914
|
+
- On Alpine Linux, compares installed kernel with running kernel
|
|
915
|
+
|
|
916
|
+
On FreeBSD systems:
|
|
917
|
+
- Compares running kernel version with installed kernel version
|
|
918
|
+
"""
|
|
919
|
+
|
|
920
|
+
@override
|
|
921
|
+
def command(self) -> str:
|
|
922
|
+
return """
|
|
923
|
+
# Get OS type
|
|
924
|
+
OS_TYPE=$(uname -s)
|
|
925
|
+
if [ "$OS_TYPE" = "Linux" ]; then
|
|
926
|
+
# Check if it's Alpine Linux
|
|
927
|
+
if [ -f /etc/alpine-release ]; then
|
|
928
|
+
RUNNING_KERNEL=$(uname -r)
|
|
929
|
+
INSTALLED_KERNEL=$(ls -1 /lib/modules | sort -V | tail -n1)
|
|
930
|
+
if [ "$RUNNING_KERNEL" != "$INSTALLED_KERNEL" ]; then
|
|
931
|
+
echo "reboot_required"
|
|
932
|
+
exit 0
|
|
933
|
+
fi
|
|
934
|
+
else
|
|
935
|
+
# Check standard Linux reboot required files
|
|
936
|
+
if [ -f /var/run/reboot-required ] || [ -f /var/run/reboot-required.pkgs ]; then
|
|
937
|
+
echo "reboot_required"
|
|
938
|
+
exit 0
|
|
939
|
+
fi
|
|
940
|
+
fi
|
|
941
|
+
elif [ "$OS_TYPE" = "FreeBSD" ]; then
|
|
942
|
+
RUNNING_VERSION=$(freebsd-version -r)
|
|
943
|
+
INSTALLED_VERSION=$(freebsd-version -k)
|
|
944
|
+
if [ "$RUNNING_VERSION" != "$INSTALLED_VERSION" ]; then
|
|
945
|
+
echo "reboot_required"
|
|
946
|
+
exit 0
|
|
947
|
+
fi
|
|
948
|
+
fi
|
|
949
|
+
echo "no_reboot_required"
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
@override
|
|
953
|
+
def process(self, output) -> bool:
|
|
954
|
+
return list(output)[0].strip() == "reboot_required"
|