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
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The server module takes care of os-level state. Targets POSIX compatibility, tested on
|
|
3
|
+
Linux/BSD.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from io import StringIO
|
|
9
|
+
from itertools import filterfalse, tee
|
|
10
|
+
from os import path
|
|
11
|
+
from time import sleep
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from pyinfra import host, logger, state
|
|
15
|
+
from pyinfra.api import FunctionCommand, OperationError, StringCommand, operation
|
|
16
|
+
from pyinfra.api.util import try_int
|
|
17
|
+
from pyinfra.connectors.util import remove_any_sudo_askpass_file
|
|
18
|
+
from pyinfra.facts.files import Directory, FindInFile, Link
|
|
19
|
+
from pyinfra.facts.server import (
|
|
20
|
+
Groups,
|
|
21
|
+
Home,
|
|
22
|
+
Hostname,
|
|
23
|
+
Kernel,
|
|
24
|
+
KernelModules,
|
|
25
|
+
Locales,
|
|
26
|
+
Mounts,
|
|
27
|
+
Os,
|
|
28
|
+
Sysctl,
|
|
29
|
+
Users,
|
|
30
|
+
Which,
|
|
31
|
+
)
|
|
32
|
+
from pyinfra.operations import crontab as crontab_
|
|
33
|
+
|
|
34
|
+
from . import (
|
|
35
|
+
apk,
|
|
36
|
+
apt,
|
|
37
|
+
brew,
|
|
38
|
+
bsdinit,
|
|
39
|
+
dnf,
|
|
40
|
+
files,
|
|
41
|
+
openrc,
|
|
42
|
+
pacman,
|
|
43
|
+
pkg,
|
|
44
|
+
runit,
|
|
45
|
+
systemd,
|
|
46
|
+
sysvinit,
|
|
47
|
+
upstart,
|
|
48
|
+
xbps,
|
|
49
|
+
yum,
|
|
50
|
+
zypper,
|
|
51
|
+
)
|
|
52
|
+
from .util.files import chmod
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from pyinfra.api.arguments_typed import PyinfraOperation
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@operation(is_idempotent=False)
|
|
59
|
+
def reboot(delay=10, interval=1, reboot_timeout=300):
|
|
60
|
+
"""
|
|
61
|
+
Reboot the server and wait for reconnection.
|
|
62
|
+
|
|
63
|
+
+ delay: number of seconds to wait before attempting reconnect
|
|
64
|
+
+ interval: interval (s) between reconnect attempts
|
|
65
|
+
+ reboot_timeout: total time before giving up reconnecting
|
|
66
|
+
|
|
67
|
+
**Example:**
|
|
68
|
+
|
|
69
|
+
.. code:: python
|
|
70
|
+
|
|
71
|
+
from pyinfra.operations import server
|
|
72
|
+
server.reboot(
|
|
73
|
+
name="Reboot the server and wait to reconnect",
|
|
74
|
+
delay=60,
|
|
75
|
+
reboot_timeout=600,
|
|
76
|
+
)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# Remove this now, before we reboot the server - if the reboot fails (expected or
|
|
80
|
+
# not) we'll error if we don't clean this up now. Will simply be re-uploaded if
|
|
81
|
+
# needed later.
|
|
82
|
+
def remove_any_askpass_file(state, host):
|
|
83
|
+
remove_any_sudo_askpass_file(host)
|
|
84
|
+
|
|
85
|
+
yield FunctionCommand(remove_any_askpass_file, (), {})
|
|
86
|
+
|
|
87
|
+
yield StringCommand("reboot", _success_exit_codes=[0, -1]) # -1 being error/disconnected
|
|
88
|
+
|
|
89
|
+
def wait_and_reconnect(state, host): # pragma: no cover
|
|
90
|
+
sleep(delay)
|
|
91
|
+
max_retries = round(reboot_timeout / interval)
|
|
92
|
+
|
|
93
|
+
host.disconnect() # make sure we are properly disconnected
|
|
94
|
+
retries = 0
|
|
95
|
+
|
|
96
|
+
while True:
|
|
97
|
+
host.connect(show_errors=False)
|
|
98
|
+
if host.connected:
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
if retries > max_retries:
|
|
102
|
+
raise Exception(
|
|
103
|
+
("Server did not reboot in time (reboot_timeout={0}s)").format(reboot_timeout),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
sleep(interval)
|
|
107
|
+
retries += 1
|
|
108
|
+
|
|
109
|
+
yield FunctionCommand(wait_and_reconnect, (), {})
|
|
110
|
+
|
|
111
|
+
# On certain systems sudo files are lost on reboot
|
|
112
|
+
def clean_sudo_info(state, host):
|
|
113
|
+
host.connector_data["sudo_askpass_path"] = None
|
|
114
|
+
|
|
115
|
+
yield FunctionCommand(clean_sudo_info, (), {})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@operation(is_idempotent=False)
|
|
119
|
+
def wait(port: int):
|
|
120
|
+
"""
|
|
121
|
+
Waits for a port to come active on the target machine. Requires netstat, checks every
|
|
122
|
+
second.
|
|
123
|
+
|
|
124
|
+
+ port: port number to wait for
|
|
125
|
+
|
|
126
|
+
**Example:**
|
|
127
|
+
|
|
128
|
+
.. code:: python
|
|
129
|
+
|
|
130
|
+
server.wait(
|
|
131
|
+
name="Wait for webserver to start",
|
|
132
|
+
port=80,
|
|
133
|
+
)
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
yield r"""
|
|
137
|
+
while ! (netstat -an | grep LISTEN | grep -e "\.{0}" -e ":{0}"); do
|
|
138
|
+
echo "waiting for port {0}..."
|
|
139
|
+
sleep 1
|
|
140
|
+
done
|
|
141
|
+
""".format(
|
|
142
|
+
port,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@operation(is_idempotent=False)
|
|
147
|
+
def shell(commands: str | list[str]):
|
|
148
|
+
"""
|
|
149
|
+
Run raw shell code on server during a deploy. If the command would
|
|
150
|
+
modify data that would be in a fact, the fact would not be updated
|
|
151
|
+
since facts are only run at the start of a deploy.
|
|
152
|
+
|
|
153
|
+
+ commands: command or list of commands to execute on the remote server
|
|
154
|
+
|
|
155
|
+
**Example:**
|
|
156
|
+
|
|
157
|
+
.. code:: python
|
|
158
|
+
|
|
159
|
+
server.shell(
|
|
160
|
+
name="Run lxd auto init",
|
|
161
|
+
commands=["lxd init --auto"],
|
|
162
|
+
)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# Ensure we have a list
|
|
166
|
+
if isinstance(commands, str):
|
|
167
|
+
commands = [commands]
|
|
168
|
+
|
|
169
|
+
for command in commands:
|
|
170
|
+
yield command
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@operation(is_idempotent=False)
|
|
174
|
+
def script(src: str, args=()):
|
|
175
|
+
"""
|
|
176
|
+
Upload and execute a local script on the remote host.
|
|
177
|
+
|
|
178
|
+
+ src: local script filename to upload & execute
|
|
179
|
+
+ args: iterable to pass as arguments to the script
|
|
180
|
+
|
|
181
|
+
**Example:**
|
|
182
|
+
|
|
183
|
+
.. code:: python
|
|
184
|
+
|
|
185
|
+
# Note: This assumes there is a file in files/hello.bash locally.
|
|
186
|
+
server.script(
|
|
187
|
+
name="Hello",
|
|
188
|
+
src="files/hello.bash",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Example passing arguments to the script
|
|
192
|
+
server.script(
|
|
193
|
+
name="Hello",
|
|
194
|
+
src="files/hello.bash",
|
|
195
|
+
args=("do-something", "with-this"),
|
|
196
|
+
)
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
temp_file = host.get_temp_filename()
|
|
200
|
+
yield from files.put._inner(src=src, dest=temp_file)
|
|
201
|
+
|
|
202
|
+
yield chmod(temp_file, "+x")
|
|
203
|
+
yield StringCommand(temp_file, *args)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@operation(is_idempotent=False)
|
|
207
|
+
def script_template(src: str, args=(), **data):
|
|
208
|
+
"""
|
|
209
|
+
Generate, upload and execute a local script template on the remote host.
|
|
210
|
+
|
|
211
|
+
+ src: local script template filename
|
|
212
|
+
|
|
213
|
+
**Example:**
|
|
214
|
+
|
|
215
|
+
.. code:: python
|
|
216
|
+
|
|
217
|
+
# Example showing how to pass python variable to a script template file.
|
|
218
|
+
# The .j2 file can use `{{ some_var }}` to be interpolated.
|
|
219
|
+
# To see output need to run pyinfra with '-v'
|
|
220
|
+
# Note: This assumes there is a file in templates/hello2.bash.j2 locally.
|
|
221
|
+
some_var = 'blah blah blah '
|
|
222
|
+
server.script_template(
|
|
223
|
+
name="Hello from script",
|
|
224
|
+
src="templates/hello2.bash.j2",
|
|
225
|
+
some_var=some_var,
|
|
226
|
+
)
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
temp_file = host.get_temp_filename("{0}{1}".format(src, data))
|
|
230
|
+
yield from files.template._inner(src, temp_file, **data)
|
|
231
|
+
|
|
232
|
+
yield chmod(temp_file, "+x")
|
|
233
|
+
yield StringCommand(temp_file, *args)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@operation()
|
|
237
|
+
def modprobe(module: str, present=True, force=False):
|
|
238
|
+
"""
|
|
239
|
+
Load/unload kernel modules.
|
|
240
|
+
|
|
241
|
+
+ module: name of the module to manage
|
|
242
|
+
+ present: whether the module should be loaded or not
|
|
243
|
+
+ force: whether to force any add/remove modules
|
|
244
|
+
|
|
245
|
+
**Example:**
|
|
246
|
+
|
|
247
|
+
.. code:: python
|
|
248
|
+
|
|
249
|
+
server.modprobe(
|
|
250
|
+
name="Silly example for modprobe",
|
|
251
|
+
module="floppy",
|
|
252
|
+
)
|
|
253
|
+
"""
|
|
254
|
+
list_value = [module] if isinstance(module, str) else module
|
|
255
|
+
|
|
256
|
+
# NOTE: https://docs.python.org/3/library/itertools.html#itertools-recipes
|
|
257
|
+
def partition(predicate, iterable):
|
|
258
|
+
t1, t2 = tee(iterable)
|
|
259
|
+
return list(filter(predicate, t2)), list(filterfalse(predicate, t1))
|
|
260
|
+
|
|
261
|
+
modules = host.get_fact(KernelModules)
|
|
262
|
+
present_mods, missing_mods = partition(lambda mod: mod in modules, list_value)
|
|
263
|
+
|
|
264
|
+
args = ""
|
|
265
|
+
if force:
|
|
266
|
+
args = " -f"
|
|
267
|
+
|
|
268
|
+
# Module is loaded and we don't want it?
|
|
269
|
+
if not present and present_mods:
|
|
270
|
+
yield "modprobe{0} -r -a {1}".format(args, " ".join(present_mods))
|
|
271
|
+
|
|
272
|
+
# Module isn't loaded and we want it?
|
|
273
|
+
elif present and missing_mods:
|
|
274
|
+
yield "modprobe{0} -a {1}".format(args, " ".join(missing_mods))
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
host.noop(
|
|
278
|
+
"{0} {1} {2} {3}".format(
|
|
279
|
+
"modules" if len(list_value) > 1 else "module",
|
|
280
|
+
"/".join(list_value),
|
|
281
|
+
"are" if len(list_value) > 1 else "is",
|
|
282
|
+
"loaded" if present else "not loaded",
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@operation()
|
|
288
|
+
def mount(
|
|
289
|
+
path: str,
|
|
290
|
+
mounted=True,
|
|
291
|
+
options: list[str] | None = None,
|
|
292
|
+
device: str | None = None,
|
|
293
|
+
fs_type: str | None = None,
|
|
294
|
+
# TODO: do we want to manage fstab here?
|
|
295
|
+
# update_fstab=False,
|
|
296
|
+
):
|
|
297
|
+
"""
|
|
298
|
+
Manage mounted filesystems.
|
|
299
|
+
|
|
300
|
+
+ path: the path of the mounted filesystem
|
|
301
|
+
+ mounted: whether the filesystem should be mounted
|
|
302
|
+
+ options: the mount options
|
|
303
|
+
|
|
304
|
+
Options:
|
|
305
|
+
If the currently mounted filesystem does not have all of the provided
|
|
306
|
+
options it will be remounted with the options provided.
|
|
307
|
+
|
|
308
|
+
``/etc/fstab``:
|
|
309
|
+
This operation does not attempt to modify the on disk fstab file - for
|
|
310
|
+
that you should use the `files.line operation <./files.html#files-line>`_.
|
|
311
|
+
"""
|
|
312
|
+
options = options or []
|
|
313
|
+
options_string = ",".join(options)
|
|
314
|
+
|
|
315
|
+
mounts = host.get_fact(Mounts)
|
|
316
|
+
is_mounted = path in mounts
|
|
317
|
+
|
|
318
|
+
# Want mount but don't have?
|
|
319
|
+
if mounted and not is_mounted:
|
|
320
|
+
args = []
|
|
321
|
+
if fs_type:
|
|
322
|
+
args.extend(["-t", fs_type])
|
|
323
|
+
if options_string:
|
|
324
|
+
args.extend(["-o", options_string])
|
|
325
|
+
if device:
|
|
326
|
+
args.append(device)
|
|
327
|
+
args.append(path)
|
|
328
|
+
|
|
329
|
+
yield StringCommand("mount", *args)
|
|
330
|
+
|
|
331
|
+
# Want no mount but mounted?
|
|
332
|
+
elif mounted is False and is_mounted:
|
|
333
|
+
yield "umount {0}".format(path)
|
|
334
|
+
|
|
335
|
+
# Want mount and is mounted! Check the options
|
|
336
|
+
elif is_mounted and mounted and options:
|
|
337
|
+
mounted_options = mounts[path]["options"]
|
|
338
|
+
needed_options = set(options) - set(mounted_options)
|
|
339
|
+
if needed_options:
|
|
340
|
+
if host.get_fact(Kernel).strip() == "FreeBSD":
|
|
341
|
+
fs_type = mounts[path]["type"]
|
|
342
|
+
device = mounts[path]["device"]
|
|
343
|
+
|
|
344
|
+
yield "mount -o update,{options} -t {fs_type} {device} {path}".format(
|
|
345
|
+
options=options_string, fs_type=fs_type, device=device, path=path
|
|
346
|
+
)
|
|
347
|
+
else:
|
|
348
|
+
yield "mount -o remount,{0} {1}".format(options_string, path)
|
|
349
|
+
|
|
350
|
+
else:
|
|
351
|
+
host.noop(
|
|
352
|
+
"filesystem {0} is {1}".format(
|
|
353
|
+
path,
|
|
354
|
+
"mounted" if mounted else "not mounted",
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@operation()
|
|
360
|
+
def hostname(hostname: str, hostname_file: str | None = None):
|
|
361
|
+
"""
|
|
362
|
+
Set the system hostname using ``hostnamectl`` or ``hostname`` on older systems.
|
|
363
|
+
|
|
364
|
+
+ hostname: the hostname that should be set
|
|
365
|
+
+ hostname_file: the file that permanently sets the hostname
|
|
366
|
+
|
|
367
|
+
Hostname file:
|
|
368
|
+
The hostname file only matters no systems that do not have ``hostnamectl``,
|
|
369
|
+
which is part of ``systemd``.
|
|
370
|
+
|
|
371
|
+
By default pyinfra will auto detect this by targeting ``/etc/hostname``
|
|
372
|
+
on Linux and ``/etc/myname`` on OpenBSD.
|
|
373
|
+
|
|
374
|
+
To completely disable writing the hostname file, set ``hostname_file=False``.
|
|
375
|
+
|
|
376
|
+
**Example:**
|
|
377
|
+
|
|
378
|
+
.. code:: python
|
|
379
|
+
|
|
380
|
+
server.hostname(
|
|
381
|
+
name="Set the hostname",
|
|
382
|
+
hostname="server1.example.com",
|
|
383
|
+
)
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
current_hostname = host.get_fact(Hostname)
|
|
387
|
+
|
|
388
|
+
if host.get_fact(Which, command="hostnamectl"):
|
|
389
|
+
if current_hostname != hostname:
|
|
390
|
+
yield "hostnamectl set-hostname {0}".format(hostname)
|
|
391
|
+
else:
|
|
392
|
+
host.noop("hostname is set")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
if hostname_file is None:
|
|
396
|
+
os = host.get_fact(Os)
|
|
397
|
+
|
|
398
|
+
if os == "Linux":
|
|
399
|
+
hostname_file = "/etc/hostname"
|
|
400
|
+
elif os == "OpenBSD":
|
|
401
|
+
hostname_file = "/etc/myname"
|
|
402
|
+
|
|
403
|
+
if current_hostname != hostname:
|
|
404
|
+
yield "hostname {0}".format(hostname)
|
|
405
|
+
else:
|
|
406
|
+
host.noop("hostname is set")
|
|
407
|
+
|
|
408
|
+
if hostname_file:
|
|
409
|
+
# Create a whole new hostname file
|
|
410
|
+
file = StringIO("{0}\n".format(hostname))
|
|
411
|
+
|
|
412
|
+
# And ensure it exists
|
|
413
|
+
yield from files.put._inner(src=file, dest=hostname_file)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@operation()
|
|
417
|
+
def sysctl(
|
|
418
|
+
key: str,
|
|
419
|
+
value: str | int | list[str | int],
|
|
420
|
+
persist=False,
|
|
421
|
+
persist_file="/etc/sysctl.conf",
|
|
422
|
+
):
|
|
423
|
+
"""
|
|
424
|
+
Edit sysctl configuration.
|
|
425
|
+
|
|
426
|
+
+ key: name of the sysctl setting to ensure
|
|
427
|
+
+ value: the value or list of values the sysctl should be
|
|
428
|
+
+ persist: whether to write this sysctl to the config
|
|
429
|
+
+ persist_file: file to write the sysctl to persist on reboot
|
|
430
|
+
|
|
431
|
+
**Example:**
|
|
432
|
+
|
|
433
|
+
.. code:: python
|
|
434
|
+
|
|
435
|
+
server.sysctl(
|
|
436
|
+
name="Change the fs.file-max value",
|
|
437
|
+
key="fs.file-max",
|
|
438
|
+
value=100000,
|
|
439
|
+
persist=True,
|
|
440
|
+
)
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
string_value = " ".join(["{0}".format(v) for v in value]) if isinstance(value, list) else value
|
|
444
|
+
|
|
445
|
+
value = [try_int(v) for v in value] if isinstance(value, list) else try_int(value)
|
|
446
|
+
|
|
447
|
+
existing_sysctls = host.get_fact(Sysctl, keys=[key])
|
|
448
|
+
existing_value = existing_sysctls.get(key)
|
|
449
|
+
|
|
450
|
+
if existing_value != value:
|
|
451
|
+
yield "sysctl {0}='{1}'".format(key, string_value)
|
|
452
|
+
else:
|
|
453
|
+
host.noop("sysctl {0} is set to {1}".format(key, string_value))
|
|
454
|
+
|
|
455
|
+
if persist:
|
|
456
|
+
yield from files.line._inner(
|
|
457
|
+
path=persist_file,
|
|
458
|
+
line="{0}[[:space:]]*=[[:space:]]*{1}".format(key, string_value),
|
|
459
|
+
replace="{0} = {1}".format(key, string_value),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@operation()
|
|
464
|
+
def service(
|
|
465
|
+
service: str,
|
|
466
|
+
running=True,
|
|
467
|
+
restarted=False,
|
|
468
|
+
reloaded=False,
|
|
469
|
+
command: str | None = None,
|
|
470
|
+
enabled: bool | None = None,
|
|
471
|
+
):
|
|
472
|
+
"""
|
|
473
|
+
Manage the state of services. This command checks for the presence of all the
|
|
474
|
+
Linux init systems pyinfra can handle and executes the relevant operation.
|
|
475
|
+
|
|
476
|
+
+ service: name of the service to manage
|
|
477
|
+
+ running: whether the service should be running
|
|
478
|
+
+ restarted: whether the service should be restarted
|
|
479
|
+
+ reloaded: whether the service should be reloaded
|
|
480
|
+
+ command: custom command execute
|
|
481
|
+
+ enabled: whether this service should be enabled/disabled on boot
|
|
482
|
+
|
|
483
|
+
**Example:**
|
|
484
|
+
|
|
485
|
+
.. code:: python
|
|
486
|
+
|
|
487
|
+
server.service(
|
|
488
|
+
name="Enable open-vm-tools service",
|
|
489
|
+
service="open-vm-tools",
|
|
490
|
+
enabled=True,
|
|
491
|
+
)
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
service_operation: "PyinfraOperation"
|
|
495
|
+
|
|
496
|
+
if host.get_fact(Which, command="systemctl"):
|
|
497
|
+
service_operation = systemd.service
|
|
498
|
+
|
|
499
|
+
elif host.get_fact(Which, command="rc-service"):
|
|
500
|
+
service_operation = openrc.service
|
|
501
|
+
|
|
502
|
+
elif host.get_fact(Which, command="initctl"):
|
|
503
|
+
service_operation = upstart.service
|
|
504
|
+
|
|
505
|
+
elif host.get_fact(Which, command="sv"):
|
|
506
|
+
service_operation = runit.service
|
|
507
|
+
|
|
508
|
+
elif (
|
|
509
|
+
host.get_fact(Which, command="service")
|
|
510
|
+
or host.get_fact(Link, path="/etc/init.d")
|
|
511
|
+
or host.get_fact(Directory, path="/etc/init.d")
|
|
512
|
+
):
|
|
513
|
+
service_operation = sysvinit.service
|
|
514
|
+
|
|
515
|
+
# NOTE: important that we are not Linux here because /etc/rc.d will exist but checking it's
|
|
516
|
+
# contents may trigger things (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
|
|
517
|
+
elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
|
|
518
|
+
service_operation = bsdinit.service
|
|
519
|
+
|
|
520
|
+
else:
|
|
521
|
+
raise OperationError(
|
|
522
|
+
("No init system found (no systemctl, initctl, /etc/init.d or /etc/rc.d found)"),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
yield from service_operation._inner(
|
|
526
|
+
service=service,
|
|
527
|
+
running=running,
|
|
528
|
+
restarted=restarted,
|
|
529
|
+
reloaded=reloaded,
|
|
530
|
+
command=command,
|
|
531
|
+
enabled=enabled,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@operation()
|
|
536
|
+
def packages(
|
|
537
|
+
packages: str | list[str],
|
|
538
|
+
present=True,
|
|
539
|
+
):
|
|
540
|
+
"""
|
|
541
|
+
Add or remove system packages. This command checks for the presence of all the
|
|
542
|
+
system package managers pyinfra can handle and executes the relevant operation.
|
|
543
|
+
|
|
544
|
+
+ packages: list of packages to ensure
|
|
545
|
+
+ present: whether the packages should be installed
|
|
546
|
+
|
|
547
|
+
**Example:**
|
|
548
|
+
|
|
549
|
+
.. code:: python
|
|
550
|
+
|
|
551
|
+
server.packages(
|
|
552
|
+
name="Install Vim and vimpager",
|
|
553
|
+
packages=["vimpager", "vim"],
|
|
554
|
+
)
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
package_operation: "PyinfraOperation"
|
|
558
|
+
|
|
559
|
+
# TODO: improve this - use LinuxDistribution fact + mapping with fallback below?
|
|
560
|
+
# Here to be preferred on openSUSE which also provides aptitude
|
|
561
|
+
# See: https://github.com/Fizzadar/pyinfra/issues/799
|
|
562
|
+
if host.get_fact(Which, command="zypper"):
|
|
563
|
+
package_operation = zypper.packages
|
|
564
|
+
|
|
565
|
+
elif host.get_fact(Which, command="apk"):
|
|
566
|
+
package_operation = apk.packages
|
|
567
|
+
|
|
568
|
+
elif host.get_fact(Which, command="apt"):
|
|
569
|
+
package_operation = apt.packages
|
|
570
|
+
|
|
571
|
+
elif host.get_fact(Which, command="brew"):
|
|
572
|
+
package_operation = brew.packages
|
|
573
|
+
|
|
574
|
+
elif host.get_fact(Which, command="dnf"):
|
|
575
|
+
package_operation = dnf.packages
|
|
576
|
+
|
|
577
|
+
elif host.get_fact(Which, command="pacman"):
|
|
578
|
+
package_operation = pacman.packages
|
|
579
|
+
|
|
580
|
+
elif host.get_fact(Which, command="xbps-install") or host.get_fact(Which, command="xbps"):
|
|
581
|
+
package_operation = xbps.packages
|
|
582
|
+
|
|
583
|
+
elif host.get_fact(Which, command="yum"):
|
|
584
|
+
package_operation = yum.packages
|
|
585
|
+
|
|
586
|
+
elif host.get_fact(Which, command="pkg") or host.get_fact(Which, command="pkg_add"):
|
|
587
|
+
package_operation = pkg.packages
|
|
588
|
+
|
|
589
|
+
else:
|
|
590
|
+
raise OperationError(
|
|
591
|
+
(
|
|
592
|
+
"No system package manager found "
|
|
593
|
+
"(no apk, apt, brew, dnf, pacman, pkg, xbps, yum or zypper found)"
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
yield from package_operation._inner(packages=packages, present=present)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
crontab = crontab_.crontab
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@operation()
|
|
604
|
+
def group(group: str, present=True, system=False, gid: int | str | None = None):
|
|
605
|
+
"""
|
|
606
|
+
Add/remove system groups.
|
|
607
|
+
|
|
608
|
+
+ group: name of the group to ensure
|
|
609
|
+
+ present: whether the group should be present or not
|
|
610
|
+
+ system: whether to create a system group
|
|
611
|
+
+ gid: use a specific groupid number
|
|
612
|
+
|
|
613
|
+
System users:
|
|
614
|
+
System users don't exist on BSD, so the argument is ignored for BSD targets.
|
|
615
|
+
|
|
616
|
+
**Examples:**
|
|
617
|
+
|
|
618
|
+
.. code:: python
|
|
619
|
+
|
|
620
|
+
server.group(
|
|
621
|
+
name="Create docker group",
|
|
622
|
+
group="docker",
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# multiple groups
|
|
626
|
+
for group in ["wheel", "lusers"]:
|
|
627
|
+
server.group(
|
|
628
|
+
name=f"Create the group {group}",
|
|
629
|
+
group=group,
|
|
630
|
+
)
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
groups = host.get_fact(Groups)
|
|
634
|
+
os_type = host.get_fact(Os)
|
|
635
|
+
is_present = group in groups
|
|
636
|
+
|
|
637
|
+
# Group exists but we don't want them?
|
|
638
|
+
if not present and is_present:
|
|
639
|
+
if os_type == "FreeBSD":
|
|
640
|
+
yield "pw groupdel -n {0}".format(group)
|
|
641
|
+
else:
|
|
642
|
+
yield "groupdel {0}".format(group)
|
|
643
|
+
|
|
644
|
+
# Group doesn't exist and we want it?
|
|
645
|
+
elif present and not is_present:
|
|
646
|
+
args = []
|
|
647
|
+
|
|
648
|
+
# BSD doesn't do system users
|
|
649
|
+
if system and "BSD" not in host.get_fact(Os):
|
|
650
|
+
args.append("-r")
|
|
651
|
+
|
|
652
|
+
if os_type == "FreeBSD":
|
|
653
|
+
args.append("-n {0}".format(group))
|
|
654
|
+
else:
|
|
655
|
+
args.append(group)
|
|
656
|
+
|
|
657
|
+
if gid:
|
|
658
|
+
if os_type == "FreeBSD":
|
|
659
|
+
args.append("-g {0}".format(gid))
|
|
660
|
+
else:
|
|
661
|
+
args.append("--gid {0}".format(gid))
|
|
662
|
+
|
|
663
|
+
# Groups are often added by other operations (package installs), so check
|
|
664
|
+
# for the group at runtime before adding.
|
|
665
|
+
group_add_command = "groupadd"
|
|
666
|
+
if os_type == "FreeBSD":
|
|
667
|
+
group_add_command = "pw groupadd"
|
|
668
|
+
yield "{0} {1}".format(group_add_command, " ".join(args))
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
@operation()
|
|
672
|
+
def user_authorized_keys(
|
|
673
|
+
user: str,
|
|
674
|
+
public_keys: str | list[str],
|
|
675
|
+
group: str | None = None,
|
|
676
|
+
delete_keys=False,
|
|
677
|
+
authorized_key_directory: str | None = None,
|
|
678
|
+
authorized_key_filename: str | None = None,
|
|
679
|
+
):
|
|
680
|
+
"""
|
|
681
|
+
Manage `authorized_keys` of system users.
|
|
682
|
+
|
|
683
|
+
+ user: name of the user to ensure
|
|
684
|
+
+ public_keys: list of public keys to attach to this user, ``home`` must be specified
|
|
685
|
+
+ group: the users primary group
|
|
686
|
+
+ delete_keys: whether to remove any keys not specified in ``public_keys``
|
|
687
|
+
|
|
688
|
+
Public keys:
|
|
689
|
+
These can be provided as strings containing the public key or as a path to
|
|
690
|
+
a public key file which pyinfra will read.
|
|
691
|
+
|
|
692
|
+
**Examples:**
|
|
693
|
+
|
|
694
|
+
.. code:: python
|
|
695
|
+
|
|
696
|
+
server.user_authorized_keys(
|
|
697
|
+
name="Ensure user has a public key",
|
|
698
|
+
user="kevin",
|
|
699
|
+
public_keys=["ed25519..."],
|
|
700
|
+
)
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
if not authorized_key_directory:
|
|
704
|
+
home = host.get_fact(Home, user=user)
|
|
705
|
+
authorized_key_directory = f"{home}/.ssh"
|
|
706
|
+
|
|
707
|
+
if not authorized_key_filename:
|
|
708
|
+
authorized_key_filename = "authorized_keys"
|
|
709
|
+
|
|
710
|
+
if isinstance(public_keys, str):
|
|
711
|
+
public_keys = [public_keys]
|
|
712
|
+
|
|
713
|
+
def read_any_pub_key_file(key):
|
|
714
|
+
try_path = key
|
|
715
|
+
if state.cwd:
|
|
716
|
+
try_path = path.join(state.cwd, key)
|
|
717
|
+
|
|
718
|
+
if path.exists(try_path):
|
|
719
|
+
with open(try_path, "r") as f:
|
|
720
|
+
return [key.strip() for key in f.readlines()]
|
|
721
|
+
|
|
722
|
+
return [key.strip()]
|
|
723
|
+
|
|
724
|
+
public_keys = [key for key_or_file in public_keys for key in read_any_pub_key_file(key_or_file)]
|
|
725
|
+
|
|
726
|
+
# Ensure .ssh directory
|
|
727
|
+
# note that this always outputs commands unless the SSH user has access to the
|
|
728
|
+
# authorized_keys file, ie the SSH user is the user defined in this function
|
|
729
|
+
yield from files.directory._inner(
|
|
730
|
+
path=authorized_key_directory,
|
|
731
|
+
user=user,
|
|
732
|
+
group=group or user,
|
|
733
|
+
mode=700,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
authorized_key_file = f"{authorized_key_directory}/{authorized_key_filename}"
|
|
737
|
+
|
|
738
|
+
if delete_keys:
|
|
739
|
+
# Create a whole new authorized_keys file
|
|
740
|
+
keys_file = StringIO(
|
|
741
|
+
"{0}\n".format(
|
|
742
|
+
"\n".join(public_keys),
|
|
743
|
+
),
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# And ensure it exists
|
|
747
|
+
yield from files.put._inner(
|
|
748
|
+
src=keys_file,
|
|
749
|
+
dest=authorized_key_file,
|
|
750
|
+
user=user,
|
|
751
|
+
group=group or user,
|
|
752
|
+
mode=600,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
else:
|
|
756
|
+
# Ensure authorized_keys exists
|
|
757
|
+
yield from files.file._inner(
|
|
758
|
+
path=authorized_key_file,
|
|
759
|
+
user=user,
|
|
760
|
+
group=group or user,
|
|
761
|
+
mode=600,
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# And every public key is present
|
|
765
|
+
for key in public_keys:
|
|
766
|
+
yield from files.line._inner(path=authorized_key_file, line=key, ensure_newline=True)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
@operation()
|
|
770
|
+
def user(
|
|
771
|
+
user: str,
|
|
772
|
+
present=True,
|
|
773
|
+
home: str | None = None,
|
|
774
|
+
shell: str | None = None,
|
|
775
|
+
group: str | None = None,
|
|
776
|
+
groups: list[str] | None = None,
|
|
777
|
+
append=False,
|
|
778
|
+
public_keys: str | list[str] | None = None,
|
|
779
|
+
delete_keys=False,
|
|
780
|
+
ensure_home=True,
|
|
781
|
+
create_home=False,
|
|
782
|
+
system=False,
|
|
783
|
+
uid: int | None = None,
|
|
784
|
+
comment: str | None = None,
|
|
785
|
+
unique=True,
|
|
786
|
+
password: str | None = None,
|
|
787
|
+
):
|
|
788
|
+
"""
|
|
789
|
+
Add/remove/update system users & their ssh `authorized_keys`.
|
|
790
|
+
|
|
791
|
+
+ user: name of the user to ensure
|
|
792
|
+
+ present: whether this user should exist
|
|
793
|
+
+ home: the users home directory
|
|
794
|
+
+ shell: the users shell
|
|
795
|
+
+ group: the users primary group
|
|
796
|
+
+ groups: the users secondary groups
|
|
797
|
+
+ append: whether to add `user` to `groups`, w/o losing membership of other groups
|
|
798
|
+
+ public_keys: list of public keys to attach to this user, ``home`` must be specified
|
|
799
|
+
+ delete_keys: whether to remove any keys not specified in ``public_keys``
|
|
800
|
+
+ ensure_home: whether to ensure the ``home`` directory exists
|
|
801
|
+
+ create_home: whether user create new user home directories from the system skeleton
|
|
802
|
+
+ system: whether to create a system account
|
|
803
|
+
+ uid: use a specific userid number
|
|
804
|
+
+ comment: the user GECOS comment
|
|
805
|
+
+ unique: prevent creating users with duplicate UID
|
|
806
|
+
+ password: set the encrypted password for the user
|
|
807
|
+
|
|
808
|
+
Home directory:
|
|
809
|
+
When ``ensure_home`` or ``public_keys`` are provided, ``home`` defaults to
|
|
810
|
+
``/home/{name}``. When ``create_home`` is ``True`` any newly created users
|
|
811
|
+
will be created with the ``-m`` flag to build a new home directory from the
|
|
812
|
+
systems skeleton directory.
|
|
813
|
+
|
|
814
|
+
Public keys:
|
|
815
|
+
These can be provided as strings containing the public key or as a path to
|
|
816
|
+
a public key file which pyinfra will read.
|
|
817
|
+
|
|
818
|
+
**Examples:**
|
|
819
|
+
|
|
820
|
+
.. code:: python
|
|
821
|
+
|
|
822
|
+
server.user(
|
|
823
|
+
name="Ensure user is removed",
|
|
824
|
+
user="kevin",
|
|
825
|
+
present=False,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
server.user(
|
|
829
|
+
name="Ensure myweb user exists",
|
|
830
|
+
user="myweb",
|
|
831
|
+
shell="/bin/bash",
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# multiple users
|
|
835
|
+
for user in ["kevin", "bob"]:
|
|
836
|
+
server.user(
|
|
837
|
+
name=f"Ensure user {user} is removed",
|
|
838
|
+
user=user,
|
|
839
|
+
present=False,
|
|
840
|
+
)
|
|
841
|
+
"""
|
|
842
|
+
|
|
843
|
+
users = host.get_fact(Users)
|
|
844
|
+
existing_groups = host.get_fact(Groups)
|
|
845
|
+
existing_user = users.get(user)
|
|
846
|
+
os_type = host.get_fact(Os)
|
|
847
|
+
if groups is None:
|
|
848
|
+
groups = []
|
|
849
|
+
|
|
850
|
+
if home is None:
|
|
851
|
+
home = "/home/{0}".format(user)
|
|
852
|
+
if existing_user:
|
|
853
|
+
home = existing_user.get("home", home)
|
|
854
|
+
|
|
855
|
+
# User not wanted?
|
|
856
|
+
if not present:
|
|
857
|
+
if existing_user:
|
|
858
|
+
if os_type == "FreeBSD":
|
|
859
|
+
yield "pw userdel -n {0}".format(user)
|
|
860
|
+
else:
|
|
861
|
+
yield "userdel {0}".format(user)
|
|
862
|
+
return
|
|
863
|
+
|
|
864
|
+
# User doesn't exist but we want them?
|
|
865
|
+
if present and existing_user is None:
|
|
866
|
+
# Fix the case where a group of the same name already exists, tell useradd to use this
|
|
867
|
+
# group rather than failing trying to create it.
|
|
868
|
+
if not group and user in existing_groups:
|
|
869
|
+
group = user
|
|
870
|
+
|
|
871
|
+
# Create the user w/home/shell
|
|
872
|
+
args = []
|
|
873
|
+
|
|
874
|
+
if home:
|
|
875
|
+
args.append("-d {0}".format(home))
|
|
876
|
+
|
|
877
|
+
if shell:
|
|
878
|
+
args.append("-s {0}".format(shell))
|
|
879
|
+
|
|
880
|
+
if group:
|
|
881
|
+
args.append("-g {0}".format(group))
|
|
882
|
+
|
|
883
|
+
if groups:
|
|
884
|
+
args.append("-G {0}".format(",".join(groups)))
|
|
885
|
+
|
|
886
|
+
if system and "BSD" not in host.get_fact(Os):
|
|
887
|
+
args.append("-r")
|
|
888
|
+
|
|
889
|
+
if uid:
|
|
890
|
+
if os_type == "FreeBSD":
|
|
891
|
+
args.append("-u {0}".format(uid))
|
|
892
|
+
else:
|
|
893
|
+
args.append("--uid {0}".format(uid))
|
|
894
|
+
|
|
895
|
+
if comment:
|
|
896
|
+
args.append("-c '{0}'".format(comment))
|
|
897
|
+
|
|
898
|
+
if not unique:
|
|
899
|
+
args.append("-o")
|
|
900
|
+
|
|
901
|
+
if create_home:
|
|
902
|
+
args.append("-m")
|
|
903
|
+
elif os_type != "FreeBSD":
|
|
904
|
+
args.append("-M")
|
|
905
|
+
|
|
906
|
+
if password and os_type != "FreeBSD":
|
|
907
|
+
args.append("-p '{0}'".format(password))
|
|
908
|
+
|
|
909
|
+
# Users are often added by other operations (package installs), so check
|
|
910
|
+
# for the user at runtime before adding.
|
|
911
|
+
add_user_command = "useradd"
|
|
912
|
+
|
|
913
|
+
if os_type == "FreeBSD":
|
|
914
|
+
add_user_command = "pw useradd"
|
|
915
|
+
|
|
916
|
+
if password:
|
|
917
|
+
yield "echo '{3}' | {0} -n {2} -H 0 {1}".format(
|
|
918
|
+
add_user_command, " ".join(args), user, password
|
|
919
|
+
)
|
|
920
|
+
else:
|
|
921
|
+
yield "{0} -n {2} {1}".format(
|
|
922
|
+
add_user_command,
|
|
923
|
+
" ".join(args),
|
|
924
|
+
user,
|
|
925
|
+
)
|
|
926
|
+
else:
|
|
927
|
+
yield "{0} {1} {2}".format(
|
|
928
|
+
add_user_command,
|
|
929
|
+
" ".join(args),
|
|
930
|
+
user,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
# User exists and we want them, check home/shell/keys/password
|
|
934
|
+
else:
|
|
935
|
+
args = []
|
|
936
|
+
|
|
937
|
+
# Check homedir
|
|
938
|
+
if home and existing_user["home"] != home:
|
|
939
|
+
args.append("-d {0}".format(home))
|
|
940
|
+
|
|
941
|
+
# Check shell
|
|
942
|
+
if shell and existing_user["shell"] != shell:
|
|
943
|
+
args.append("-s {0}".format(shell))
|
|
944
|
+
|
|
945
|
+
# Check primary group
|
|
946
|
+
if group and existing_user["group"] != group:
|
|
947
|
+
args.append("-g {0}".format(group))
|
|
948
|
+
|
|
949
|
+
# Check secondary groups, if defined
|
|
950
|
+
if groups:
|
|
951
|
+
if append:
|
|
952
|
+
if not set(groups).issubset(existing_user["groups"]):
|
|
953
|
+
args.append("-a")
|
|
954
|
+
args.append("-G {0}".format(",".join(groups)))
|
|
955
|
+
elif set(existing_user["groups"]) != set(groups):
|
|
956
|
+
args.append("-G {0}".format(",".join(groups)))
|
|
957
|
+
|
|
958
|
+
if comment and existing_user["comment"] != comment:
|
|
959
|
+
args.append("-c '{0}'".format(comment))
|
|
960
|
+
|
|
961
|
+
if password and existing_user["password"] != password:
|
|
962
|
+
if os_type == "FreeBSD":
|
|
963
|
+
yield "echo '{0}' | pw usermod -n {1} -H 0".format(password, user)
|
|
964
|
+
else:
|
|
965
|
+
args.append("-p '{0}'".format(password))
|
|
966
|
+
|
|
967
|
+
# Need to mod the user?
|
|
968
|
+
if args:
|
|
969
|
+
if os_type == "FreeBSD":
|
|
970
|
+
yield "pw usermod -n {1} {0}".format(" ".join(args), user)
|
|
971
|
+
else:
|
|
972
|
+
yield "usermod {0} {1}".format(" ".join(args), user)
|
|
973
|
+
|
|
974
|
+
# Ensure home directory ownership
|
|
975
|
+
if ensure_home and home:
|
|
976
|
+
yield from files.directory._inner(
|
|
977
|
+
path=home,
|
|
978
|
+
user=user,
|
|
979
|
+
group=group or user,
|
|
980
|
+
# Don't fail if the home directory exists as a link
|
|
981
|
+
_no_fail_on_link=True,
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
# Add SSH keys
|
|
985
|
+
if public_keys is not None:
|
|
986
|
+
yield from user_authorized_keys._inner(
|
|
987
|
+
user=user,
|
|
988
|
+
public_keys=public_keys,
|
|
989
|
+
group=group,
|
|
990
|
+
delete_keys=delete_keys,
|
|
991
|
+
authorized_key_directory="{0}/.ssh".format(home),
|
|
992
|
+
authorized_key_filename=None,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
@operation()
|
|
997
|
+
def locale(
|
|
998
|
+
locale: str,
|
|
999
|
+
present=True,
|
|
1000
|
+
):
|
|
1001
|
+
"""
|
|
1002
|
+
Enable/Disable locale.
|
|
1003
|
+
|
|
1004
|
+
+ locale: name of the locale to enable/disable
|
|
1005
|
+
+ present: whether this locale should be present or not
|
|
1006
|
+
|
|
1007
|
+
**Examples:**
|
|
1008
|
+
|
|
1009
|
+
.. code:: python
|
|
1010
|
+
|
|
1011
|
+
server.locale(
|
|
1012
|
+
name="Ensure en_GB.UTF-8 locale is not present",
|
|
1013
|
+
locale="en_GB.UTF-8",
|
|
1014
|
+
present=False,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
server.locale(
|
|
1018
|
+
name="Ensure en_GB.UTF-8 locale is present",
|
|
1019
|
+
locale="en_GB.UTF-8",
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
"""
|
|
1023
|
+
|
|
1024
|
+
locales = host.get_fact(Locales)
|
|
1025
|
+
|
|
1026
|
+
logger.debug("Enabled locales: {0}".format(locales))
|
|
1027
|
+
|
|
1028
|
+
locales_definitions_file = "/etc/locale.gen"
|
|
1029
|
+
|
|
1030
|
+
# Find the matching line in /etc/locale.gen
|
|
1031
|
+
matching_lines = host.get_fact(
|
|
1032
|
+
FindInFile, path=locales_definitions_file, pattern=rf"^.*{locale}[[:space:]]\+.*$"
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
if not matching_lines:
|
|
1036
|
+
raise OperationError(f"Locale {locale} not found in {locales_definitions_file}")
|
|
1037
|
+
|
|
1038
|
+
if len(matching_lines) > 1:
|
|
1039
|
+
raise OperationError(f"Multiple locales matches for {locale} in {locales_definitions_file}")
|
|
1040
|
+
|
|
1041
|
+
matching_line = matching_lines[0]
|
|
1042
|
+
|
|
1043
|
+
# Remove locale
|
|
1044
|
+
if not present and locale in locales:
|
|
1045
|
+
logger.debug(f"Removing locale {locale}")
|
|
1046
|
+
|
|
1047
|
+
yield from files.line._inner(
|
|
1048
|
+
path=locales_definitions_file, line=f"^{matching_line}$", replace=f"# {matching_line}"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
yield "locale-gen"
|
|
1052
|
+
|
|
1053
|
+
# Add locale
|
|
1054
|
+
if present and locale not in locales:
|
|
1055
|
+
logger.debug(f"Adding locale {locale}")
|
|
1056
|
+
|
|
1057
|
+
yield from files.replace._inner(
|
|
1058
|
+
path=locales_definitions_file,
|
|
1059
|
+
text=f"^{matching_line}$",
|
|
1060
|
+
replace=f"{matching_line}".replace("# ", ""),
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
yield "locale-gen"
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
@operation()
|
|
1067
|
+
def security_limit(
|
|
1068
|
+
domain: str,
|
|
1069
|
+
limit_type: str,
|
|
1070
|
+
item: str,
|
|
1071
|
+
value: int,
|
|
1072
|
+
):
|
|
1073
|
+
"""
|
|
1074
|
+
Edit /etc/security/limits.conf configuration.
|
|
1075
|
+
|
|
1076
|
+
+ domain: the domain (user, group, or wildcard) for the limit
|
|
1077
|
+
+ limit_type: the type of limit (hard or soft)
|
|
1078
|
+
+ item: the item to limit (e.g., nofile, nproc)
|
|
1079
|
+
+ value: the value for the limit
|
|
1080
|
+
|
|
1081
|
+
**Example:**
|
|
1082
|
+
|
|
1083
|
+
.. code:: python
|
|
1084
|
+
|
|
1085
|
+
security_limit(
|
|
1086
|
+
name="Set nofile limit for all users",
|
|
1087
|
+
domain='*',
|
|
1088
|
+
limit_type='soft',
|
|
1089
|
+
item='nofile',
|
|
1090
|
+
value=1024,
|
|
1091
|
+
)
|
|
1092
|
+
"""
|
|
1093
|
+
|
|
1094
|
+
line_format = f"{domain}\t{limit_type}\t{item}\t{value}"
|
|
1095
|
+
|
|
1096
|
+
yield from files.line._inner(
|
|
1097
|
+
path="/etc/security/limits.conf",
|
|
1098
|
+
line=f"^{domain}[[:space:]]+{limit_type}[[:space:]]+{item}",
|
|
1099
|
+
replace=line_format,
|
|
1100
|
+
)
|