pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +115 -97
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +5 -3
- pyinfra/api/config.py +139 -39
- pyinfra/api/connectors.py +5 -2
- pyinfra/api/deploy.py +19 -19
- pyinfra/api/exceptions.py +35 -4
- pyinfra/api/facts.py +62 -86
- pyinfra/api/host.py +102 -15
- pyinfra/api/inventory.py +4 -0
- pyinfra/api/operation.py +188 -120
- pyinfra/api/operations.py +66 -113
- pyinfra/api/state.py +53 -34
- pyinfra/api/util.py +64 -33
- pyinfra/connectors/base.py +65 -20
- pyinfra/connectors/chroot.py +15 -13
- pyinfra/connectors/docker.py +62 -72
- pyinfra/connectors/dockerssh.py +20 -19
- pyinfra/connectors/local.py +32 -22
- pyinfra/connectors/ssh.py +162 -86
- pyinfra/connectors/sshuserclient/client.py +1 -1
- pyinfra/connectors/terraform.py +57 -39
- pyinfra/connectors/util.py +26 -27
- pyinfra/connectors/vagrant.py +27 -26
- pyinfra/context.py +1 -0
- pyinfra/facts/apk.py +7 -2
- pyinfra/facts/apt.py +15 -7
- pyinfra/facts/brew.py +28 -13
- pyinfra/facts/bsdinit.py +9 -6
- pyinfra/facts/cargo.py +6 -3
- pyinfra/facts/choco.py +8 -4
- pyinfra/facts/deb.py +21 -9
- pyinfra/facts/dnf.py +11 -6
- pyinfra/facts/docker.py +30 -5
- pyinfra/facts/files.py +49 -33
- pyinfra/facts/gem.py +7 -2
- pyinfra/facts/git.py +14 -21
- pyinfra/facts/gpg.py +4 -1
- pyinfra/facts/hardware.py +186 -138
- pyinfra/facts/launchd.py +7 -2
- pyinfra/facts/lxd.py +8 -2
- pyinfra/facts/mysql.py +19 -12
- pyinfra/facts/npm.py +3 -1
- pyinfra/facts/openrc.py +8 -2
- pyinfra/facts/pacman.py +13 -5
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +5 -1
- pyinfra/facts/pkgin.py +7 -2
- pyinfra/facts/postgres.py +170 -0
- pyinfra/facts/postgresql.py +5 -162
- pyinfra/facts/rpm.py +21 -15
- pyinfra/facts/runit.py +70 -0
- pyinfra/facts/selinux.py +12 -4
- pyinfra/facts/server.py +240 -82
- pyinfra/facts/snap.py +8 -2
- pyinfra/facts/systemd.py +37 -13
- pyinfra/facts/sysvinit.py +7 -4
- pyinfra/facts/upstart.py +7 -2
- pyinfra/facts/util/packaging.py +3 -2
- pyinfra/facts/vzctl.py +8 -4
- pyinfra/facts/xbps.py +7 -2
- pyinfra/facts/yum.py +10 -5
- pyinfra/facts/zypper.py +9 -4
- pyinfra/operations/apk.py +5 -3
- pyinfra/operations/apt.py +28 -25
- pyinfra/operations/brew.py +60 -29
- pyinfra/operations/bsdinit.py +6 -4
- pyinfra/operations/cargo.py +3 -1
- pyinfra/operations/choco.py +3 -1
- pyinfra/operations/dnf.py +16 -20
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +187 -168
- pyinfra/operations/gem.py +3 -1
- pyinfra/operations/git.py +23 -25
- pyinfra/operations/iptables.py +33 -25
- pyinfra/operations/launchd.py +5 -6
- pyinfra/operations/lxd.py +7 -4
- pyinfra/operations/mysql.py +59 -55
- pyinfra/operations/npm.py +8 -1
- pyinfra/operations/openrc.py +5 -3
- pyinfra/operations/pacman.py +6 -7
- pyinfra/operations/pip.py +19 -12
- pyinfra/operations/pkg.py +3 -1
- pyinfra/operations/pkgin.py +5 -3
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -335
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -29
- pyinfra/operations/server.py +138 -67
- pyinfra/operations/snap.py +3 -1
- pyinfra/operations/ssh.py +18 -16
- pyinfra/operations/systemd.py +18 -12
- pyinfra/operations/sysvinit.py +7 -5
- pyinfra/operations/upstart.py +7 -5
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +177 -0
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +54 -38
- pyinfra/operations/util/service.py +39 -47
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +5 -3
- pyinfra/operations/yum.py +15 -19
- pyinfra/operations/zypper.py +9 -10
- pyinfra/version.py +5 -2
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
- pyinfra-3.0.2.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +3 -2
- pyinfra_cli/exceptions.py +75 -43
- pyinfra_cli/inventory.py +52 -31
- pyinfra_cli/log.py +10 -2
- pyinfra_cli/main.py +88 -65
- pyinfra_cli/prints.py +37 -109
- pyinfra_cli/util.py +15 -10
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +9 -9
- tests/test_api/test_api_deploys.py +15 -19
- tests/test_api/test_api_facts.py +4 -5
- tests/test_api/test_api_operations.py +18 -20
- tests/test_api/test_api_util.py +41 -2
- tests/test_cli/test_cli.py +14 -50
- tests/test_cli/test_cli_deploy.py +17 -14
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/test_cli_inventory.py +66 -0
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_dockerssh.py +11 -8
- tests/test_connectors/test_ssh.py +88 -23
- tests/test_connectors/test_sshuserclient.py +1 -1
- tests/test_connectors/test_terraform.py +11 -8
- tests/test_connectors/test_vagrant.py +6 -6
- pyinfra/connectors/ansible.py +0 -175
- pyinfra/connectors/mech.py +0 -189
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -312
- pyinfra/facts/windows.py +0 -366
- pyinfra/facts/windows_files.py +0 -90
- pyinfra/operations/windows.py +0 -59
- pyinfra/operations/windows_files.py +0 -538
- pyinfra-3.0.dev0.dist-info/RECORD +0 -170
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
pyinfra/operations/selinux.py
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Provides operations to set SELinux file contexts, booleans and port types.
|
|
3
3
|
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
4
9
|
from pyinfra import host
|
|
5
|
-
from pyinfra.api import QuoteString, StringCommand, operation
|
|
10
|
+
from pyinfra.api import OperationValueError, QuoteString, StringCommand, operation
|
|
6
11
|
from pyinfra.facts.selinux import FileContext, FileContextMapping, SEBoolean, SEPort, SEPorts
|
|
7
12
|
from pyinfra.facts.server import Which
|
|
8
13
|
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
class Boolean(Enum):
|
|
16
|
+
ON = "on"
|
|
17
|
+
OFF = "off"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Protocol(Enum):
|
|
21
|
+
UDP = "udp"
|
|
22
|
+
TCP = "tcp"
|
|
23
|
+
SCTP = "sctp"
|
|
24
|
+
DCCP = "dccp"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@operation()
|
|
28
|
+
def boolean(bool_name: str, value: Boolean, persistent=False):
|
|
14
29
|
"""
|
|
15
30
|
Set the specified SELinux boolean to the desired state.
|
|
16
31
|
|
|
17
32
|
+ boolean: name of an SELinux boolean
|
|
18
|
-
+
|
|
33
|
+
+ value: desired state of the boolean
|
|
19
34
|
+ persistent: whether to write updated policy or not
|
|
20
35
|
|
|
21
36
|
Note: This operation requires root privileges.
|
|
@@ -27,28 +42,31 @@ def boolean(bool_name, value, persistent=False):
|
|
|
27
42
|
selinux.boolean(
|
|
28
43
|
name='Allow Apache to connect to LDAP server',
|
|
29
44
|
'httpd_can_network_connect',
|
|
30
|
-
|
|
45
|
+
Boolean.ON,
|
|
31
46
|
persistent=True
|
|
32
47
|
)
|
|
33
48
|
"""
|
|
34
|
-
_valid_states = ["on", "off"]
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
value_str: str
|
|
51
|
+
if value in ["on", "off"]: # compatibility with the old version
|
|
52
|
+
assert isinstance(value, str)
|
|
53
|
+
value_str = value
|
|
54
|
+
elif value is Boolean.ON:
|
|
55
|
+
value_str = "on"
|
|
56
|
+
elif value is Boolean.OFF:
|
|
57
|
+
value_str = "off"
|
|
58
|
+
else:
|
|
59
|
+
raise OperationValueError(f"Invalid value '{value}' for boolean operation")
|
|
40
60
|
|
|
41
|
-
if host.get_fact(SEBoolean, boolean=bool_name) !=
|
|
61
|
+
if host.get_fact(SEBoolean, boolean=bool_name) != value_str:
|
|
42
62
|
persist = "-P " if persistent else ""
|
|
43
|
-
yield StringCommand("setsebool", f"{persist}{bool_name}",
|
|
63
|
+
yield StringCommand("setsebool", f"{persist}{bool_name}", value_str)
|
|
44
64
|
else:
|
|
45
|
-
host.noop(f"boolean '{bool_name}' already had the value '{
|
|
65
|
+
host.noop(f"boolean '{bool_name}' already had the value '{value_str}'")
|
|
46
66
|
|
|
47
67
|
|
|
48
|
-
@operation(
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
def file_context(path, se_type):
|
|
68
|
+
@operation()
|
|
69
|
+
def file_context(path: str, se_type: str):
|
|
52
70
|
"""
|
|
53
71
|
Set the SELinux type for the specified path to the specified value.
|
|
54
72
|
|
|
@@ -73,10 +91,8 @@ def file_context(path, se_type):
|
|
|
73
91
|
host.noop(f"file_context: '{path}' already had type '{se_type}'")
|
|
74
92
|
|
|
75
93
|
|
|
76
|
-
@operation(
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
def file_context_mapping(target, se_type=None, present=True):
|
|
94
|
+
@operation()
|
|
95
|
+
def file_context_mapping(target: str, se_type: str | None = None, present=True):
|
|
80
96
|
"""
|
|
81
97
|
Set the SELinux file context mapping for paths matching the target.
|
|
82
98
|
|
|
@@ -115,10 +131,8 @@ def file_context_mapping(target, se_type=None, present=True):
|
|
|
115
131
|
host.noop(f"no existing mapping for '{target}'")
|
|
116
132
|
|
|
117
133
|
|
|
118
|
-
@operation(
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
def port(protocol, port_num, se_type=None, present=True):
|
|
134
|
+
@operation()
|
|
135
|
+
def port(protocol: Protocol | str, port_num: int, se_type: str | None = None, present=True):
|
|
122
136
|
"""
|
|
123
137
|
Set the SELinux type for the specified protocol and port.
|
|
124
138
|
|
|
@@ -135,17 +149,21 @@ def port(protocol, port_num, se_type=None, present=True):
|
|
|
135
149
|
|
|
136
150
|
selinux.port(
|
|
137
151
|
name='Allow Apache to provide service on port 2222',
|
|
138
|
-
|
|
152
|
+
Protocol.TCP,
|
|
139
153
|
2222,
|
|
140
154
|
'http_port_t',
|
|
141
155
|
)
|
|
142
156
|
"""
|
|
143
157
|
|
|
158
|
+
if protocol is Protocol:
|
|
159
|
+
assert isinstance(protocol, Protocol)
|
|
160
|
+
protocol = protocol.value
|
|
161
|
+
|
|
144
162
|
if present and (se_type is None):
|
|
145
163
|
raise ValueError("se_type must have a valid value if present is set")
|
|
146
164
|
|
|
147
165
|
new_type = se_type if present else ""
|
|
148
|
-
direct_get = len(host.get_fact(Which, command=
|
|
166
|
+
direct_get = len(host.get_fact(Which, command="sepolicy") or "") > 0
|
|
149
167
|
if direct_get:
|
|
150
168
|
current = host.get_fact(SEPort, protocol=protocol, port=port_num)
|
|
151
169
|
else:
|
pyinfra/operations/server.py
CHANGED
|
@@ -3,12 +3,14 @@ The server module takes care of os-level state. Targets POSIX compatibility, tes
|
|
|
3
3
|
Linux/BSD.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import shlex
|
|
7
9
|
from io import StringIO
|
|
8
10
|
from itertools import filterfalse, tee
|
|
9
11
|
from os import path
|
|
10
12
|
from time import sleep
|
|
11
|
-
from typing import
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
12
14
|
|
|
13
15
|
from pyinfra import host, logger, state
|
|
14
16
|
from pyinfra.api import FunctionCommand, OperationError, StringCommand, operation
|
|
@@ -18,6 +20,7 @@ from pyinfra.facts.files import Directory, FindInFile, Link
|
|
|
18
20
|
from pyinfra.facts.server import (
|
|
19
21
|
Crontab,
|
|
20
22
|
Groups,
|
|
23
|
+
Home,
|
|
21
24
|
Hostname,
|
|
22
25
|
KernelModules,
|
|
23
26
|
Locales,
|
|
@@ -38,6 +41,7 @@ from . import (
|
|
|
38
41
|
openrc,
|
|
39
42
|
pacman,
|
|
40
43
|
pkg,
|
|
44
|
+
runit,
|
|
41
45
|
systemd,
|
|
42
46
|
sysvinit,
|
|
43
47
|
upstart,
|
|
@@ -47,6 +51,9 @@ from . import (
|
|
|
47
51
|
)
|
|
48
52
|
from .util.files import chmod, sed_replace
|
|
49
53
|
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from pyinfra.api.arguments_typed import PyinfraOperation
|
|
56
|
+
|
|
50
57
|
|
|
51
58
|
@operation(is_idempotent=False)
|
|
52
59
|
def reboot(delay=10, interval=1, reboot_timeout=300):
|
|
@@ -100,6 +107,12 @@ def reboot(delay=10, interval=1, reboot_timeout=300):
|
|
|
100
107
|
|
|
101
108
|
yield FunctionCommand(wait_and_reconnect, (), {})
|
|
102
109
|
|
|
110
|
+
# On certain systems sudo files are lost on reboot
|
|
111
|
+
def clean_sudo_info(state, host):
|
|
112
|
+
host.connector_data["sudo_askpass_path"] = None
|
|
113
|
+
|
|
114
|
+
yield FunctionCommand(clean_sudo_info, (), {})
|
|
115
|
+
|
|
103
116
|
|
|
104
117
|
@operation(is_idempotent=False)
|
|
105
118
|
def wait(port: int):
|
|
@@ -130,7 +143,7 @@ def wait(port: int):
|
|
|
130
143
|
|
|
131
144
|
|
|
132
145
|
@operation(is_idempotent=False)
|
|
133
|
-
def shell(commands):
|
|
146
|
+
def shell(commands: str | list[str]):
|
|
134
147
|
"""
|
|
135
148
|
Run raw shell code on server during a deploy. If the command would
|
|
136
149
|
modify data that would be in a fact, the fact would not be updated
|
|
@@ -157,7 +170,7 @@ def shell(commands):
|
|
|
157
170
|
|
|
158
171
|
|
|
159
172
|
@operation(is_idempotent=False)
|
|
160
|
-
def script(src, args=()):
|
|
173
|
+
def script(src: str, args=()):
|
|
161
174
|
"""
|
|
162
175
|
Upload and execute a local script on the remote host.
|
|
163
176
|
|
|
@@ -182,15 +195,15 @@ def script(src, args=()):
|
|
|
182
195
|
)
|
|
183
196
|
"""
|
|
184
197
|
|
|
185
|
-
temp_file =
|
|
186
|
-
yield from files.put(src=src, dest=temp_file)
|
|
198
|
+
temp_file = host.get_temp_filename()
|
|
199
|
+
yield from files.put._inner(src=src, dest=temp_file)
|
|
187
200
|
|
|
188
201
|
yield chmod(temp_file, "+x")
|
|
189
202
|
yield StringCommand(temp_file, *args)
|
|
190
203
|
|
|
191
204
|
|
|
192
205
|
@operation(is_idempotent=False)
|
|
193
|
-
def script_template(src, args=(), **data):
|
|
206
|
+
def script_template(src: str, args=(), **data):
|
|
194
207
|
"""
|
|
195
208
|
Generate, upload and execute a local script template on the remote host.
|
|
196
209
|
|
|
@@ -212,15 +225,15 @@ def script_template(src, args=(), **data):
|
|
|
212
225
|
)
|
|
213
226
|
"""
|
|
214
227
|
|
|
215
|
-
temp_file =
|
|
216
|
-
yield from files.template(src, temp_file, **data)
|
|
228
|
+
temp_file = host.get_temp_filename("{0}{1}".format(src, data))
|
|
229
|
+
yield from files.template._inner(src, temp_file, **data)
|
|
217
230
|
|
|
218
231
|
yield chmod(temp_file, "+x")
|
|
219
232
|
yield StringCommand(temp_file, *args)
|
|
220
233
|
|
|
221
234
|
|
|
222
235
|
@operation()
|
|
223
|
-
def modprobe(module, present=True, force=False):
|
|
236
|
+
def modprobe(module: str, present=True, force=False):
|
|
224
237
|
"""
|
|
225
238
|
Load/unload kernel modules.
|
|
226
239
|
|
|
@@ -272,11 +285,11 @@ def modprobe(module, present=True, force=False):
|
|
|
272
285
|
|
|
273
286
|
@operation()
|
|
274
287
|
def mount(
|
|
275
|
-
path,
|
|
288
|
+
path: str,
|
|
276
289
|
mounted=True,
|
|
277
|
-
options=None,
|
|
278
|
-
device=None,
|
|
279
|
-
fs_type=None,
|
|
290
|
+
options: list[str] | None = None,
|
|
291
|
+
device: str | None = None,
|
|
292
|
+
fs_type: str | None = None,
|
|
280
293
|
# TODO: do we want to manage fstab here?
|
|
281
294
|
# update_fstab=False,
|
|
282
295
|
):
|
|
@@ -335,7 +348,7 @@ def mount(
|
|
|
335
348
|
|
|
336
349
|
|
|
337
350
|
@operation()
|
|
338
|
-
def hostname(hostname, hostname_file=None):
|
|
351
|
+
def hostname(hostname: str, hostname_file: str | None = None):
|
|
339
352
|
"""
|
|
340
353
|
Set the system hostname using ``hostnamectl`` or ``hostname`` on older systems.
|
|
341
354
|
|
|
@@ -388,13 +401,13 @@ def hostname(hostname, hostname_file=None):
|
|
|
388
401
|
file = StringIO("{0}\n".format(hostname))
|
|
389
402
|
|
|
390
403
|
# And ensure it exists
|
|
391
|
-
yield from files.put(src=file, dest=hostname_file)
|
|
404
|
+
yield from files.put._inner(src=file, dest=hostname_file)
|
|
392
405
|
|
|
393
406
|
|
|
394
407
|
@operation()
|
|
395
408
|
def sysctl(
|
|
396
|
-
key,
|
|
397
|
-
value,
|
|
409
|
+
key: str,
|
|
410
|
+
value: str | int | list[str | int],
|
|
398
411
|
persist=False,
|
|
399
412
|
persist_file="/etc/sysctl.conf",
|
|
400
413
|
):
|
|
@@ -422,16 +435,16 @@ def sysctl(
|
|
|
422
435
|
|
|
423
436
|
value = [try_int(v) for v in value] if isinstance(value, list) else try_int(value)
|
|
424
437
|
|
|
425
|
-
existing_sysctls = host.get_fact(Sysctl)
|
|
426
|
-
|
|
438
|
+
existing_sysctls = host.get_fact(Sysctl, keys=[key])
|
|
427
439
|
existing_value = existing_sysctls.get(key)
|
|
440
|
+
|
|
428
441
|
if not existing_value or existing_value != value:
|
|
429
442
|
yield "sysctl {0}='{1}'".format(key, string_value)
|
|
430
443
|
else:
|
|
431
444
|
host.noop("sysctl {0} is set to {1}".format(key, string_value))
|
|
432
445
|
|
|
433
446
|
if persist:
|
|
434
|
-
yield from files.line(
|
|
447
|
+
yield from files.line._inner(
|
|
435
448
|
path=persist_file,
|
|
436
449
|
line="{0}[[:space:]]*=[[:space:]]*{1}".format(key, string_value),
|
|
437
450
|
replace="{0} = {1}".format(key, string_value),
|
|
@@ -440,16 +453,16 @@ def sysctl(
|
|
|
440
453
|
|
|
441
454
|
@operation()
|
|
442
455
|
def service(
|
|
443
|
-
service,
|
|
456
|
+
service: str,
|
|
444
457
|
running=True,
|
|
445
458
|
restarted=False,
|
|
446
459
|
reloaded=False,
|
|
447
|
-
command=None,
|
|
448
|
-
enabled=None,
|
|
460
|
+
command: str | None = None,
|
|
461
|
+
enabled: bool | None = None,
|
|
449
462
|
):
|
|
450
463
|
"""
|
|
451
464
|
Manage the state of services. This command checks for the presence of all the
|
|
452
|
-
Linux init systems
|
|
465
|
+
Linux init systems pyinfra can handle and executes the relevant operation.
|
|
453
466
|
|
|
454
467
|
+ service: name of the service to manage
|
|
455
468
|
+ running: whether the service should be running
|
|
@@ -469,7 +482,7 @@ def service(
|
|
|
469
482
|
)
|
|
470
483
|
"""
|
|
471
484
|
|
|
472
|
-
service_operation:
|
|
485
|
+
service_operation: "PyinfraOperation"
|
|
473
486
|
|
|
474
487
|
if host.get_fact(Which, command="systemctl"):
|
|
475
488
|
service_operation = systemd.service
|
|
@@ -480,6 +493,9 @@ def service(
|
|
|
480
493
|
elif host.get_fact(Which, command="initctl"):
|
|
481
494
|
service_operation = upstart.service
|
|
482
495
|
|
|
496
|
+
elif host.get_fact(Which, command="sv"):
|
|
497
|
+
service_operation = runit.service
|
|
498
|
+
|
|
483
499
|
elif (
|
|
484
500
|
host.get_fact(Which, command="service")
|
|
485
501
|
or host.get_fact(Link, path="/etc/init.d")
|
|
@@ -489,7 +505,7 @@ def service(
|
|
|
489
505
|
|
|
490
506
|
# NOTE: important that we are not Linux here because /etc/rc.d will exist but checking it's
|
|
491
507
|
# contents may trigger things (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
|
|
492
|
-
elif host.get_fact(Os) != "Linux" and host.get_fact(Directory, path="/etc/rc.d"):
|
|
508
|
+
elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
|
|
493
509
|
service_operation = bsdinit.service
|
|
494
510
|
|
|
495
511
|
else:
|
|
@@ -497,7 +513,7 @@ def service(
|
|
|
497
513
|
("No init system found " "(no systemctl, initctl, /etc/init.d or /etc/rc.d found)"),
|
|
498
514
|
)
|
|
499
515
|
|
|
500
|
-
yield from service_operation(
|
|
516
|
+
yield from service_operation._inner(
|
|
501
517
|
service=service,
|
|
502
518
|
running=running,
|
|
503
519
|
restarted=restarted,
|
|
@@ -509,12 +525,12 @@ def service(
|
|
|
509
525
|
|
|
510
526
|
@operation()
|
|
511
527
|
def packages(
|
|
512
|
-
packages,
|
|
528
|
+
packages: str | list[str],
|
|
513
529
|
present=True,
|
|
514
530
|
):
|
|
515
531
|
"""
|
|
516
532
|
Add or remove system packages. This command checks for the presence of all the
|
|
517
|
-
system package managers
|
|
533
|
+
system package managers pyinfra can handle and executes the relevant operation.
|
|
518
534
|
|
|
519
535
|
+ packages: list of packages to ensure
|
|
520
536
|
+ present: whether the packages should be installed
|
|
@@ -529,7 +545,7 @@ def packages(
|
|
|
529
545
|
)
|
|
530
546
|
"""
|
|
531
547
|
|
|
532
|
-
package_operation:
|
|
548
|
+
package_operation: "PyinfraOperation"
|
|
533
549
|
|
|
534
550
|
# TODO: improve this - use LinuxDistribution fact + mapping with fallback below?
|
|
535
551
|
# Here to be preferred on openSUSE which also provides aptitude
|
|
@@ -552,7 +568,7 @@ def packages(
|
|
|
552
568
|
elif host.get_fact(Which, command="pacman"):
|
|
553
569
|
package_operation = pacman.packages
|
|
554
570
|
|
|
555
|
-
elif host.get_fact(Which, command="xbps"):
|
|
571
|
+
elif host.get_fact(Which, command="xbps-install") or host.get_fact(Which, command="xbps"):
|
|
556
572
|
package_operation = xbps.packages
|
|
557
573
|
|
|
558
574
|
elif host.get_fact(Which, command="yum"):
|
|
@@ -569,21 +585,21 @@ def packages(
|
|
|
569
585
|
),
|
|
570
586
|
)
|
|
571
587
|
|
|
572
|
-
yield from package_operation(packages=packages, present=present)
|
|
588
|
+
yield from package_operation._inner(packages=packages, present=present)
|
|
573
589
|
|
|
574
590
|
|
|
575
591
|
@operation()
|
|
576
592
|
def crontab(
|
|
577
|
-
command,
|
|
593
|
+
command: str,
|
|
578
594
|
present=True,
|
|
579
|
-
user=None,
|
|
580
|
-
cron_name=None,
|
|
595
|
+
user: str | None = None,
|
|
596
|
+
cron_name: str | None = None,
|
|
581
597
|
minute="*",
|
|
582
598
|
hour="*",
|
|
583
599
|
month="*",
|
|
584
600
|
day_of_week="*",
|
|
585
601
|
day_of_month="*",
|
|
586
|
-
special_time=None,
|
|
602
|
+
special_time: str | None = None,
|
|
587
603
|
interpolate_variables=False,
|
|
588
604
|
):
|
|
589
605
|
"""
|
|
@@ -645,6 +661,8 @@ def crontab(
|
|
|
645
661
|
|
|
646
662
|
if not existing_crontab and cron_name: # find the crontab by name if provided
|
|
647
663
|
for cmd, details in crontab.items():
|
|
664
|
+
if not details["comments"]:
|
|
665
|
+
continue
|
|
648
666
|
if name_comment in details["comments"]:
|
|
649
667
|
existing_crontab = details
|
|
650
668
|
existing_crontab_match = cmd
|
|
@@ -652,8 +670,8 @@ def crontab(
|
|
|
652
670
|
|
|
653
671
|
exists = existing_crontab is not None
|
|
654
672
|
|
|
655
|
-
edit_commands = []
|
|
656
|
-
temp_filename =
|
|
673
|
+
edit_commands: list[str | StringCommand] = []
|
|
674
|
+
temp_filename = host.get_temp_filename()
|
|
657
675
|
|
|
658
676
|
if special_time:
|
|
659
677
|
new_crontab_line = "{0} {1}".format(special_time, command)
|
|
@@ -701,6 +719,7 @@ def crontab(
|
|
|
701
719
|
|
|
702
720
|
# We have the cron and it exists, do it's details? If not, replace the line
|
|
703
721
|
elif present and exists:
|
|
722
|
+
assert existing_crontab is not None
|
|
704
723
|
if any(
|
|
705
724
|
(
|
|
706
725
|
special_time != existing_crontab.get("special_time"),
|
|
@@ -746,7 +765,7 @@ def crontab(
|
|
|
746
765
|
|
|
747
766
|
|
|
748
767
|
@operation()
|
|
749
|
-
def group(group, present=True, system=False, gid=None):
|
|
768
|
+
def group(group: str, present=True, system=False, gid: int | str | None = None):
|
|
750
769
|
"""
|
|
751
770
|
Add/remove system groups.
|
|
752
771
|
|
|
@@ -815,12 +834,12 @@ def group(group, present=True, system=False, gid=None):
|
|
|
815
834
|
|
|
816
835
|
@operation()
|
|
817
836
|
def user_authorized_keys(
|
|
818
|
-
user,
|
|
819
|
-
public_keys,
|
|
820
|
-
group=None,
|
|
837
|
+
user: str,
|
|
838
|
+
public_keys: str | list[str],
|
|
839
|
+
group: str | None = None,
|
|
821
840
|
delete_keys=False,
|
|
822
|
-
authorized_key_directory=None,
|
|
823
|
-
authorized_key_filename=None,
|
|
841
|
+
authorized_key_directory: str | None = None,
|
|
842
|
+
authorized_key_filename: str | None = None,
|
|
824
843
|
):
|
|
825
844
|
"""
|
|
826
845
|
Manage `authorized_keys` of system users.
|
|
@@ -832,7 +851,7 @@ def user_authorized_keys(
|
|
|
832
851
|
|
|
833
852
|
Public keys:
|
|
834
853
|
These can be provided as strings containing the public key or as a path to
|
|
835
|
-
a public key file which
|
|
854
|
+
a public key file which pyinfra will read.
|
|
836
855
|
|
|
837
856
|
**Examples:**
|
|
838
857
|
|
|
@@ -844,8 +863,11 @@ def user_authorized_keys(
|
|
|
844
863
|
public_keys=["ed25519..."],
|
|
845
864
|
)
|
|
846
865
|
"""
|
|
866
|
+
|
|
847
867
|
if not authorized_key_directory:
|
|
848
|
-
|
|
868
|
+
home = host.get_fact(Home, user=user)
|
|
869
|
+
authorized_key_directory = f"{home}/.ssh"
|
|
870
|
+
|
|
849
871
|
if not authorized_key_filename:
|
|
850
872
|
authorized_key_filename = "authorized_keys"
|
|
851
873
|
|
|
@@ -861,14 +883,14 @@ def user_authorized_keys(
|
|
|
861
883
|
with open(try_path, "r") as f:
|
|
862
884
|
return f.read().strip()
|
|
863
885
|
|
|
864
|
-
return key
|
|
886
|
+
return key.strip()
|
|
865
887
|
|
|
866
888
|
public_keys = list(map(read_any_pub_key_file, public_keys))
|
|
867
889
|
|
|
868
890
|
# Ensure .ssh directory
|
|
869
891
|
# note that this always outputs commands unless the SSH user has access to the
|
|
870
892
|
# authorized_keys file, ie the SSH user is the user defined in this function
|
|
871
|
-
yield from files.directory(
|
|
893
|
+
yield from files.directory._inner(
|
|
872
894
|
path=authorized_key_directory,
|
|
873
895
|
user=user,
|
|
874
896
|
group=group or user,
|
|
@@ -886,7 +908,7 @@ def user_authorized_keys(
|
|
|
886
908
|
)
|
|
887
909
|
|
|
888
910
|
# And ensure it exists
|
|
889
|
-
yield from files.put(
|
|
911
|
+
yield from files.put._inner(
|
|
890
912
|
src=keys_file,
|
|
891
913
|
dest=authorized_key_file,
|
|
892
914
|
user=user,
|
|
@@ -896,7 +918,7 @@ def user_authorized_keys(
|
|
|
896
918
|
|
|
897
919
|
else:
|
|
898
920
|
# Ensure authorized_keys exists
|
|
899
|
-
yield from files.file(
|
|
921
|
+
yield from files.file._inner(
|
|
900
922
|
path=authorized_key_file,
|
|
901
923
|
user=user,
|
|
902
924
|
group=group or user,
|
|
@@ -905,26 +927,27 @@ def user_authorized_keys(
|
|
|
905
927
|
|
|
906
928
|
# And every public key is present
|
|
907
929
|
for key in public_keys:
|
|
908
|
-
yield from files.line(path=authorized_key_file, line=key, ensure_newline=True)
|
|
930
|
+
yield from files.line._inner(path=authorized_key_file, line=key, ensure_newline=True)
|
|
909
931
|
|
|
910
932
|
|
|
911
933
|
@operation()
|
|
912
934
|
def user(
|
|
913
|
-
user,
|
|
935
|
+
user: str,
|
|
914
936
|
present=True,
|
|
915
|
-
home=None,
|
|
916
|
-
shell=None,
|
|
917
|
-
group=None,
|
|
918
|
-
groups=None,
|
|
919
|
-
public_keys=None,
|
|
937
|
+
home: str | None = None,
|
|
938
|
+
shell: str | None = None,
|
|
939
|
+
group: str | None = None,
|
|
940
|
+
groups: list[str] | None = None,
|
|
941
|
+
public_keys: str | list[str] | None = None,
|
|
920
942
|
delete_keys=False,
|
|
921
943
|
ensure_home=True,
|
|
922
944
|
create_home=False,
|
|
923
945
|
system=False,
|
|
924
|
-
uid=None,
|
|
925
|
-
comment=None,
|
|
946
|
+
uid: int | None = None,
|
|
947
|
+
comment: str | None = None,
|
|
926
948
|
add_deploy_dir=True,
|
|
927
949
|
unique=True,
|
|
950
|
+
password: str | None = None,
|
|
928
951
|
):
|
|
929
952
|
"""
|
|
930
953
|
Add/remove/update system users & their ssh `authorized_keys`.
|
|
@@ -944,6 +967,7 @@ def user(
|
|
|
944
967
|
+ comment: the user GECOS comment
|
|
945
968
|
+ add_deploy_dir: any public_key filenames are relative to the deploy directory
|
|
946
969
|
+ unique: prevent creating users with duplicate UID
|
|
970
|
+
+ password: set the encrypted password for the user
|
|
947
971
|
|
|
948
972
|
Home directory:
|
|
949
973
|
When ``ensure_home`` or ``public_keys`` are provided, ``home`` defaults to
|
|
@@ -953,7 +977,7 @@ def user(
|
|
|
953
977
|
|
|
954
978
|
Public keys:
|
|
955
979
|
These can be provided as strings containing the public key or as a path to
|
|
956
|
-
a public key file which
|
|
980
|
+
a public key file which pyinfra will read.
|
|
957
981
|
|
|
958
982
|
**Examples:**
|
|
959
983
|
|
|
@@ -1040,6 +1064,11 @@ def user(
|
|
|
1040
1064
|
|
|
1041
1065
|
if create_home:
|
|
1042
1066
|
args.append("-m")
|
|
1067
|
+
else:
|
|
1068
|
+
args.append("-M")
|
|
1069
|
+
|
|
1070
|
+
if password:
|
|
1071
|
+
args.append("-p '{0}'".format(password))
|
|
1043
1072
|
|
|
1044
1073
|
# Users are often added by other operations (package installs), so check
|
|
1045
1074
|
# for the user at runtime before adding.
|
|
@@ -1058,7 +1087,7 @@ def user(
|
|
|
1058
1087
|
user,
|
|
1059
1088
|
)
|
|
1060
1089
|
|
|
1061
|
-
# User exists and we want them, check home/shell/keys
|
|
1090
|
+
# User exists and we want them, check home/shell/keys/password
|
|
1062
1091
|
else:
|
|
1063
1092
|
args = []
|
|
1064
1093
|
|
|
@@ -1081,6 +1110,9 @@ def user(
|
|
|
1081
1110
|
if comment and existing_user["comment"] != comment:
|
|
1082
1111
|
args.append("-c '{0}'".format(comment))
|
|
1083
1112
|
|
|
1113
|
+
if password and existing_user["password"] != password:
|
|
1114
|
+
args.append("-p '{0}'".format(password))
|
|
1115
|
+
|
|
1084
1116
|
# Need to mod the user?
|
|
1085
1117
|
if args:
|
|
1086
1118
|
if os_type == "FreeBSD":
|
|
@@ -1097,10 +1129,12 @@ def user(
|
|
|
1097
1129
|
existing_user["group"] = group
|
|
1098
1130
|
if groups:
|
|
1099
1131
|
existing_user["groups"] = groups
|
|
1132
|
+
if password:
|
|
1133
|
+
existing_user["password"] = password
|
|
1100
1134
|
|
|
1101
1135
|
# Ensure home directory ownership
|
|
1102
|
-
if ensure_home:
|
|
1103
|
-
yield from files.directory(
|
|
1136
|
+
if ensure_home and home:
|
|
1137
|
+
yield from files.directory._inner(
|
|
1104
1138
|
path=home,
|
|
1105
1139
|
user=user,
|
|
1106
1140
|
group=group or user,
|
|
@@ -1110,7 +1144,7 @@ def user(
|
|
|
1110
1144
|
|
|
1111
1145
|
# Add SSH keys
|
|
1112
1146
|
if public_keys is not None:
|
|
1113
|
-
yield from user_authorized_keys(
|
|
1147
|
+
yield from user_authorized_keys._inner(
|
|
1114
1148
|
user=user,
|
|
1115
1149
|
public_keys=public_keys,
|
|
1116
1150
|
group=group,
|
|
@@ -1122,7 +1156,7 @@ def user(
|
|
|
1122
1156
|
|
|
1123
1157
|
@operation()
|
|
1124
1158
|
def locale(
|
|
1125
|
-
locale,
|
|
1159
|
+
locale: str,
|
|
1126
1160
|
present=True,
|
|
1127
1161
|
):
|
|
1128
1162
|
"""
|
|
@@ -1171,7 +1205,7 @@ def locale(
|
|
|
1171
1205
|
if not present and locale in locales:
|
|
1172
1206
|
logger.debug(f"Removing locale {locale}")
|
|
1173
1207
|
|
|
1174
|
-
yield from files.line(
|
|
1208
|
+
yield from files.line._inner(
|
|
1175
1209
|
path=locales_definitions_file, line=f"^{matching_line}$", replace=f"# {matching_line}"
|
|
1176
1210
|
)
|
|
1177
1211
|
|
|
@@ -1181,10 +1215,47 @@ def locale(
|
|
|
1181
1215
|
if present and locale not in locales:
|
|
1182
1216
|
logger.debug(f"Adding locale {locale}")
|
|
1183
1217
|
|
|
1184
|
-
yield from files.replace(
|
|
1218
|
+
yield from files.replace._inner(
|
|
1185
1219
|
path=locales_definitions_file,
|
|
1186
1220
|
text=f"^{matching_line}$",
|
|
1187
1221
|
replace=f"{matching_line}".replace("# ", ""),
|
|
1188
1222
|
)
|
|
1189
1223
|
|
|
1190
1224
|
yield "locale-gen"
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
@operation()
|
|
1228
|
+
def security_limit(
|
|
1229
|
+
domain: str,
|
|
1230
|
+
limit_type: str,
|
|
1231
|
+
item: str,
|
|
1232
|
+
value: int,
|
|
1233
|
+
):
|
|
1234
|
+
"""
|
|
1235
|
+
Edit /etc/security/limits.conf configuration.
|
|
1236
|
+
|
|
1237
|
+
+ domain: the domain (user, group, or wildcard) for the limit
|
|
1238
|
+
+ limit_type: the type of limit (hard or soft)
|
|
1239
|
+
+ item: the item to limit (e.g., nofile, nproc)
|
|
1240
|
+
+ value: the value for the limit
|
|
1241
|
+
|
|
1242
|
+
**Example:**
|
|
1243
|
+
|
|
1244
|
+
.. code:: python
|
|
1245
|
+
|
|
1246
|
+
security_limit(
|
|
1247
|
+
name="Set nofile limit for all users",
|
|
1248
|
+
domain='*',
|
|
1249
|
+
limit_type='soft',
|
|
1250
|
+
item='nofile',
|
|
1251
|
+
value=1024,
|
|
1252
|
+
)
|
|
1253
|
+
"""
|
|
1254
|
+
|
|
1255
|
+
line_format = f"{domain}\t{limit_type}\t{item}\t{value}"
|
|
1256
|
+
|
|
1257
|
+
yield from files.line._inner(
|
|
1258
|
+
path="/etc/security/limits.conf",
|
|
1259
|
+
line=f"^{domain}[[:space:]]+{limit_type}[[:space:]]+{item}",
|
|
1260
|
+
replace=line_format,
|
|
1261
|
+
)
|