Skip to content

Commit dfa0315

Browse files
committed
refactor: introduce automated change-tracking across deployers
The Deployer base class offers pyinfra file/directory/systemd helper methods which automatically tracks whether units have to be restarted, or a daemon-reload needs to happen to update systemd configuration.
1 parent 16b00da commit dfa0315

15 files changed

Lines changed: 402 additions & 612 deletions

File tree

chatmaild/src/chatmaild/tests/test_migrate_db.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def test_migration(tmp_path, example_config, caplog):
4848
assert passdb_path.stat().st_size > 10000
4949

5050
example_config.passdb_path = passdb_path
51+
# ensure logging.info records are captured regardless of global configuration
52+
caplog.set_level("INFO")
5153

5254
assert not caplog.records
5355

cmdeploy/src/cmdeploy/acmetool/__init__.py

Lines changed: 19 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import importlib.resources
2-
3-
from pyinfra.operations import apt, files, server, systemd
1+
from pyinfra.operations import apt, server
42

53
from ..basedeploy import Deployer
64

@@ -9,131 +7,48 @@ class AcmetoolDeployer(Deployer):
97
def __init__(self, email, domains):
108
self.domains = domains
119
self.email = email
12-
self.need_restart_redirector = False
13-
self.need_restart_reconcile_service = False
14-
self.need_restart_reconcile_timer = False
1510

1611
def install(self):
1712
apt.packages(
1813
name="Install acmetool",
1914
packages=["acmetool"],
2015
)
2116

22-
files.file(
23-
name="Remove old acmetool cronjob, it is replaced with systemd timer.",
24-
path="/etc/cron.d/acmetool",
25-
present=False,
26-
)
17+
self.remove_file("/etc/cron.d/acmetool")
2718

28-
files.put(
29-
name="Install acmetool hook.",
30-
src=importlib.resources.files(__package__)
31-
.joinpath("acmetool.hook")
32-
.open("rb"),
33-
dest="/etc/acme/hooks/nginx",
34-
user="root",
35-
group="root",
36-
mode="755",
37-
)
38-
files.file(
39-
name="Remove acmetool hook from the wrong location where it was previously installed.",
40-
path="/usr/lib/acme/hooks/nginx",
41-
present=False,
42-
)
19+
self.put_executable("acmetool/acmetool.hook", "/etc/acme/hooks/nginx")
20+
self.remove_file("/usr/lib/acme/hooks/nginx")
4321

4422
def configure(self):
45-
files.template(
46-
src=importlib.resources.files(__package__).joinpath(
47-
"response-file.yaml.j2"
48-
),
49-
dest="/var/lib/acme/conf/responses",
50-
user="root",
51-
group="root",
52-
mode="644",
23+
self.put_template(
24+
"acmetool/response-file.yaml.j2",
25+
"/var/lib/acme/conf/responses",
5326
email=self.email,
5427
)
5528

56-
files.template(
57-
src=importlib.resources.files(__package__).joinpath("target.yaml.j2"),
58-
dest="/var/lib/acme/conf/target",
59-
user="root",
60-
group="root",
61-
mode="644",
29+
self.put_template(
30+
"acmetool/target.yaml.j2",
31+
"/var/lib/acme/conf/target",
6232
)
6333

6434
server.shell(
6535
name=f"Remove old acmetool desired files for {self.domains[0]}",
6636
commands=[f"rm -f /var/lib/acme/desired/{self.domains[0]}-*"],
6737
)
68-
files.template(
69-
src=importlib.resources.files(__package__).joinpath("desired.yaml.j2"),
70-
dest=f"/var/lib/acme/desired/{self.domains[0]}", # 0 is mailhost TLD
71-
user="root",
72-
group="root",
73-
mode="644",
38+
self.put_template(
39+
"acmetool/desired.yaml.j2",
40+
f"/var/lib/acme/desired/{self.domains[0]}",
7441
domains=self.domains,
7542
)
7643

77-
service_file = files.put(
78-
src=importlib.resources.files(__package__).joinpath(
79-
"acmetool-redirector.service"
80-
),
81-
dest="/etc/systemd/system/acmetool-redirector.service",
82-
user="root",
83-
group="root",
84-
mode="644",
85-
)
86-
self.need_restart_redirector = service_file.changed
87-
88-
reconcile_service_file = files.put(
89-
src=importlib.resources.files(__package__).joinpath(
90-
"acmetool-reconcile.service"
91-
),
92-
dest="/etc/systemd/system/acmetool-reconcile.service",
93-
user="root",
94-
group="root",
95-
mode="644",
96-
)
97-
self.need_restart_reconcile_service = reconcile_service_file.changed
98-
99-
reconcile_timer_file = files.put(
100-
src=importlib.resources.files(__package__).joinpath(
101-
"acmetool-reconcile.timer"
102-
),
103-
dest="/etc/systemd/system/acmetool-reconcile.timer",
104-
user="root",
105-
group="root",
106-
mode="644",
107-
)
108-
self.need_restart_reconcile_timer = reconcile_timer_file.changed
44+
self.ensure_systemd_unit("acmetool/acmetool-redirector.service")
45+
self.ensure_systemd_unit("acmetool/acmetool-reconcile.service")
46+
self.ensure_systemd_unit("acmetool/acmetool-reconcile.timer")
10947

11048
def activate(self):
111-
systemd.service(
112-
name="Setup acmetool-redirector service",
113-
service="acmetool-redirector.service",
114-
running=True,
115-
enabled=True,
116-
restarted=self.need_restart_redirector,
117-
)
118-
self.need_restart_redirector = False
119-
120-
systemd.service(
121-
name="Setup acmetool-reconcile service",
122-
service="acmetool-reconcile.service",
123-
running=False,
124-
enabled=False,
125-
daemon_reload=self.need_restart_reconcile_service,
126-
)
127-
self.need_restart_reconcile_service = False
128-
129-
systemd.service(
130-
name="Setup acmetool-reconcile timer",
131-
service="acmetool-reconcile.timer",
132-
running=True,
133-
enabled=True,
134-
daemon_reload=self.need_restart_reconcile_timer,
135-
)
136-
self.need_restart_reconcile_timer = False
49+
self.ensure_service("acmetool-redirector.service")
50+
self.ensure_service("acmetool-reconcile.service", running=False, enabled=False)
51+
self.ensure_service("acmetool-reconcile.timer")
13752

13853
server.shell(
13954
name=f"Reconcile certificates for: {', '.join(self.domains)}",

cmdeploy/src/cmdeploy/basedeploy.py

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,10 @@ def get_resource(arg, pkg=__package__):
5050
return importlib.resources.files(pkg).joinpath(arg)
5151

5252

53-
def configure_remote_units(mail_domain, units) -> None:
53+
def configure_remote_units(deployer, mail_domain, units) -> None:
5454
remote_base_dir = "/usr/local/lib/chatmaild"
5555
remote_venv_dir = f"{remote_base_dir}/venv"
5656
remote_chatmail_inipath = f"{remote_base_dir}/chatmail.ini"
57-
root_owned = dict(user="root", group="root", mode="644")
5857

5958
# install systemd units
6059
for fn in units:
@@ -70,15 +69,13 @@ def configure_remote_units(mail_domain, units) -> None:
7069
source_path = get_resource(f"service/{basename}.f")
7170
content = source_path.read_text().format(**params).encode()
7271

73-
files.put(
74-
name=f"Upload {basename}",
72+
deployer.put_file(
7573
src=io.BytesIO(content),
7674
dest=f"/etc/systemd/system/{basename}",
77-
**root_owned,
7875
)
7976

8077

81-
def activate_remote_units(units) -> None:
78+
def activate_remote_units(deployer, units) -> None:
8279
# activate systemd units
8380
for fn in units:
8481
basename = fn if "." in fn else f"{fn}.service"
@@ -88,14 +85,8 @@ def activate_remote_units(units) -> None:
8885
enabled = False
8986
else:
9087
enabled = True
91-
systemd.service(
92-
name=f"Setup {basename}",
93-
service=basename,
94-
running=enabled,
95-
enabled=enabled,
96-
restarted=enabled,
97-
daemon_reload=True,
98-
)
88+
89+
deployer.ensure_service(basename, running=enabled, enabled=enabled)
9990

10091

10192
class Deployment:
@@ -141,6 +132,7 @@ def perform_stages(self, deployers):
141132

142133
class Deployer:
143134
need_restart = False
135+
daemon_reload = False
144136

145137
def install(self):
146138
pass
@@ -150,3 +142,91 @@ def configure(self):
150142

151143
def activate(self):
152144
pass
145+
146+
def ensure_service(self, service, running=True, enabled=True):
147+
if running:
148+
verb = "Start and enable"
149+
else:
150+
verb = "Stop"
151+
systemd.service(
152+
name=f"{verb} {service}",
153+
service=service,
154+
running=running,
155+
enabled=enabled,
156+
restarted=self.need_restart if running else False,
157+
daemon_reload=self.daemon_reload,
158+
)
159+
self.daemon_reload = False
160+
161+
def ensure_systemd_unit(self, src, **kwargs):
162+
dest_name = src.split("/")[-1].replace(".j2", "")
163+
dest = f"/etc/systemd/system/{dest_name}"
164+
if src.endswith(".j2"):
165+
return self.put_template(src, dest, **kwargs)
166+
return self.put_file(src, dest)
167+
168+
def put_file(self, src, dest, mode="644"):
169+
if isinstance(src, str):
170+
src = get_resource(src)
171+
res = files.put(
172+
name=f"Upload {dest}",
173+
src=src,
174+
dest=dest,
175+
user="root",
176+
group="root",
177+
mode=mode,
178+
)
179+
180+
return self._update_restart_signals(dest, res)
181+
182+
def put_executable(self, src, dest):
183+
return self.put_file(src, dest, mode="755")
184+
185+
def put_template(self, src, dest, owner="root", **kwargs):
186+
if isinstance(src, str):
187+
src = get_resource(src)
188+
res = files.template(
189+
name=f"Upload {dest}",
190+
src=src,
191+
dest=dest,
192+
user=owner,
193+
group=owner,
194+
mode="644",
195+
**kwargs,
196+
)
197+
198+
return self._update_restart_signals(dest, res)
199+
200+
def remove_file(self, dest):
201+
res = files.file(name=f"Remove {dest}", path=dest, present=False)
202+
return self._update_restart_signals(dest, res)
203+
204+
def ensure_line(self, path, line, **kwargs):
205+
name = kwargs.pop("name", f"Ensure line in {path}")
206+
res = files.line(name=name, path=path, line=line, **kwargs)
207+
return self._update_restart_signals(path, res)
208+
209+
def ensure_directory(self, path, owner="root", mode="755", **kwargs):
210+
name = kwargs.pop("name", f"Ensure directory {path}")
211+
res = files.directory(
212+
name=name,
213+
path=path,
214+
user=owner,
215+
group=owner,
216+
mode=mode,
217+
present=True,
218+
**kwargs,
219+
)
220+
return self._update_restart_signals(path, res)
221+
222+
def remove_directory(self, path, **kwargs):
223+
name = kwargs.pop("name", f"Remove directory {path}")
224+
res = files.directory(name=name, path=path, present=False, **kwargs)
225+
return self._update_restart_signals(path, res)
226+
227+
def _update_restart_signals(self, path, res):
228+
if res.changed:
229+
self.need_restart = True
230+
if str(path).startswith("/etc/systemd/system/"):
231+
self.daemon_reload = True
232+
return res

0 commit comments

Comments
 (0)