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