Merge ~cjwatson/lp-mailman:charm into lp-mailman:master

Proposed by Colin Watson
Status: Needs review
Proposed branch: ~cjwatson/lp-mailman:charm
Merge into: lp-mailman:master
Diff against target: 783 lines (+619/-48)
15 files modified
charm/.gitignore (+1/-0)
charm/lp-mailman/README.md (+7/-0)
charm/lp-mailman/charmcraft.yaml (+75/-0)
charm/lp-mailman/config.yaml (+66/-0)
charm/lp-mailman/layer.yaml (+23/-0)
charm/lp-mailman/lib/charms/lp_mailman.py (+102/-0)
charm/lp-mailman/metadata.yaml (+18/-0)
charm/lp-mailman/reactive/lp-mailman.py (+171/-0)
charm/lp-mailman/templates/crontab.j2 (+9/-0)
charm/lp-mailman/templates/logrotate.conf.j2 (+16/-0)
charm/lp-mailman/templates/lp-mailman-lazr.conf (+35/-0)
charm/lp-mailman/templates/lp-mailman-rsync.conf (+8/-0)
charm/lp-mailman/templates/lp-mailman-secrets-lazr.conf (+19/-0)
charm/lp-mailman/templates/lp-mailman.service.j2 (+16/-0)
lib/lp/services/mailman/monkeypatches/__init__.py (+53/-48)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+456439@code.launchpad.net

Commit message

Add a basic charm

Description of the change

This is incomplete. It has two major known problems:

 * lp-mailman currently wants to write its configuration on startup. This is difficult to disentangle, and it conflicts with the way our charms usually work where we deploy code as root and start it as a non-root user. I made an effort to only write configuration if it differs from the current contents, but this is fragile and I'm not sure it completely works yet. It may be worth exploring other approaches too.

 * I haven't done anything with email configuration yet. Both inbound and outbound email are of course vital for lp-mailman.

To post a comment you must log in.

Unmerged commits

27ae664... by Colin Watson

Add a basic charm

This is incomplete. It has two major known problems:

 * lp-mailman currently wants to write its configuration on startup.
   This is difficult to disentangle, and it conflicts with the way our
   charms usually work where we deploy code as root and start it as a
   non-root user. I made an effort to only write configuration if it
   differs from the current contents, but this is fragile and I'm not
   sure it completely works yet. It may be worth exploring other
   approaches too.

 * I haven't done anything with email configuration yet. Both inbound
   and outbound email are of course vital for lp-mailman.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/charm/.gitignore b/charm/.gitignore
2new file mode 100644
3index 0000000..f795aeb
4--- /dev/null
5+++ b/charm/.gitignore
6@@ -0,0 +1 @@
7+*.charm
8diff --git a/charm/lp-mailman/README.md b/charm/lp-mailman/README.md
9new file mode 100644
10index 0000000..fd1a7bb
11--- /dev/null
12+++ b/charm/lp-mailman/README.md
13@@ -0,0 +1,7 @@
14+# Launchpad mailing list manager
15+
16+This charm runs a service that operates Launchpad mailing lists.
17+
18+You will need the following relation:
19+
20+ juju relate lp-mailman rabbitmq-server
21diff --git a/charm/lp-mailman/charmcraft.yaml b/charm/lp-mailman/charmcraft.yaml
22new file mode 100644
23index 0000000..5e4bb85
24--- /dev/null
25+++ b/charm/lp-mailman/charmcraft.yaml
26@@ -0,0 +1,75 @@
27+type: charm
28+bases:
29+ - build-on:
30+ - name: ubuntu
31+ channel: "18.04"
32+ architectures: [amd64]
33+ run-on:
34+ - name: ubuntu
35+ channel: "18.04"
36+ architectures: [amd64]
37+parts:
38+ charm-wheels:
39+ source: https://git.launchpad.net/~ubuntuone-hackers/ols-charm-deps/+git/wheels
40+ source-commit: "7bcd79fa4fca485eaf15ee9e2ee16f3ab902678d"
41+ source-submodules: []
42+ source-type: git
43+ plugin: dump
44+ organize:
45+ "*": charm-wheels/
46+ stage:
47+ - charm-wheels
48+ # XXX cjwatson 2023-09-26: This is horrible, and if you can find a
49+ # better approach that works then please replace this. Ubuntu 18.04's
50+ # Python is incompatible with newer versions of pip and setuptools,
51+ # but the charm build mechanism tries to upgrade pip and setuptools in
52+ # a way that causes the old version of pip that we started with to
53+ # select an overly-new version and then complain that it's
54+ # incompatible. Removing these wheels ensures that it doesn't try,
55+ # though of course this is fragile and may break when we switch this
56+ # part to a newer source-commit.
57+ - "-charm-wheels/pip-22.0.4-py3-none-any.whl"
58+ - "-charm-wheels/setuptools-62.1.0-py3-none-any.whl"
59+ - "-charm-wheels/setuptools-65.3.0-py3-none-any.whl"
60+ prime:
61+ - "-charm-wheels"
62+ ols-layers:
63+ source: https://git.launchpad.net/ols-charm-deps
64+ source-commit: "9c59a9804f1f40e2a74be7dac9bf18a655a7864f"
65+ source-submodules: []
66+ source-type: git
67+ plugin: dump
68+ organize:
69+ "*": layers/
70+ stage:
71+ - layers
72+ prime:
73+ - "-layers"
74+ launchpad-layers:
75+ after:
76+ - ols-layers
77+ source: https://git.launchpad.net/launchpad-layers
78+ source-commit: "8b7d25a29b8297d491abe6a0c6b69f34e39dddba"
79+ source-submodules: []
80+ source-type: git
81+ plugin: dump
82+ organize:
83+ launchpad-payload: layers/layer/launchpad-payload
84+ stage:
85+ - layers
86+ prime:
87+ - "-layers"
88+ charm:
89+ after:
90+ - charm-wheels
91+ - launchpad-layers
92+ source: .
93+ plugin: reactive
94+ build-snaps: [charm]
95+ build-packages: [python3-dev]
96+ build-environment:
97+ - CHARM_LAYERS_DIR: $CRAFT_STAGE/layers/layer
98+ - CHARM_INTERFACES_DIR: $CRAFT_STAGE/layers/interface
99+ - PIP_NO_INDEX: "true"
100+ - PIP_FIND_LINKS: $CRAFT_STAGE/charm-wheels
101+ reactive-charm-build-arguments: [--binary-wheels-from-source]
102diff --git a/charm/lp-mailman/config.yaml b/charm/lp-mailman/config.yaml
103new file mode 100644
104index 0000000..e9badd8
105--- /dev/null
106+++ b/charm/lp-mailman/config.yaml
107@@ -0,0 +1,66 @@
108+options:
109+ active:
110+ type: boolean
111+ default: true
112+ description: If true, run the lp-mailman service.
113+ bounce_address:
114+ type: string
115+ description: Envelope sender address for outgoing email.
116+ default: "noreply@launchpad.test"
117+ domain:
118+ type: string
119+ description: Domain name for this instance.
120+ default: "launchpad.test"
121+ domain_lists:
122+ type: string
123+ description: Domain name for this instance's mailing list service.
124+ default: "lists.launchpad.test"
125+ domain_xmlrpc_private:
126+ type: string
127+ description: Domain name for this instance's private XML-RPC service.
128+ default: "xmlrpc-private.launchpad.test"
129+ log_hosts_allow:
130+ type: string
131+ description: >
132+ Hosts that should be allowed to rsync logs. Note that this relies on
133+ the nrpe subordinate charm to set up /etc/rsyncd.conf properly.
134+ default: ""
135+ mailman_shared_secret:
136+ type: string
137+ description: >
138+ Shared secret used to pre-approve Launchpad-generated messages to
139+ Launchpad mailing lists.
140+ default: ""
141+ oops_prefix:
142+ type: string
143+ description: A prefix for OOPS codes for this instance.
144+ default: "TEST"
145+ port_xmlrpc:
146+ type: int
147+ description: Port for the XML-RPC application server.
148+ default: 8087
149+ rabbitmq_broker_urls:
150+ type: string
151+ description: >
152+ Space-separated list of RabbitMQ broker URLs. If unset, rely on the
153+ rabbitmq relation instead.
154+ default: ""
155+ rabbitmq_user:
156+ type: string
157+ description: Username to use with RabbitMQ.
158+ default: "test"
159+ site_list_owner:
160+ type: string
161+ description: >
162+ Site list owner email address and password, separated by a colon.
163+ (This isn't actually used for anything significant because we
164+ administer lists via Launchpad instead, but Mailman requires us to
165+ provide something here. As a result, providing a default isn't too
166+ risky.)
167+ default: "listowner@launchpad.test:secret"
168+ site_message:
169+ type: string
170+ description: >
171+ If provided, this message will appear in the site status bar at the
172+ top of every page.
173+ default: ""
174diff --git a/charm/lp-mailman/layer.yaml b/charm/lp-mailman/layer.yaml
175new file mode 100644
176index 0000000..1e8d8fa
177--- /dev/null
178+++ b/charm/lp-mailman/layer.yaml
179@@ -0,0 +1,23 @@
180+includes:
181+ - interface:rabbitmq
182+ - layer:launchpad-payload
183+options:
184+ apt:
185+ packages:
186+ - python
187+ - python-dev
188+ - virtualenv
189+ ols:
190+ service_name: lp-mailman
191+ config_filename: service.conf
192+ user: launchpad
193+ tarball_payload: true
194+ symlink_switch_payload: true
195+ python_bin: python2.7
196+ launchpad-payload:
197+ # Stub out create_virtualenv by giving it a target that does nothing,
198+ # because buildmailman.py does some odd monkey-patching of the source
199+ # tree which has to be run with the correct configuration, so we need to
200+ # do that later.
201+ build_target: setup.py
202+repo: https://git.launchpad.net/lp-mailman
203diff --git a/charm/lp-mailman/lib/charms/lp_mailman.py b/charm/lp-mailman/lib/charms/lp_mailman.py
204new file mode 100644
205index 0000000..d285bbf
206--- /dev/null
207+++ b/charm/lp-mailman/lib/charms/lp_mailman.py
208@@ -0,0 +1,102 @@
209+# Copyright 2023 Canonical Ltd. This software is licensed under the
210+# GNU Affero General Public License version 3 (see the file LICENSE).
211+
212+import os.path
213+import subprocess
214+from urllib.parse import urlparse
215+
216+from charmhelpers.core import hookenv, host, templating
217+from charms.launchpad.payload import get_service_config as get_payload_config
218+from charms.reactive import endpoint_from_flag
219+from ols import base
220+
221+
222+def oopses_dir():
223+ return os.path.join(base.base_dir(), "oopses")
224+
225+
226+def secrets_dir():
227+ return os.path.join(base.base_dir(), "secrets")
228+
229+
230+def var_dir():
231+ return os.path.join(base.base_dir(), "var")
232+
233+
234+def change_shell(user, shell):
235+ if (
236+ subprocess.run(
237+ ["getent", "passwd", user],
238+ stdout=subprocess.PIPE,
239+ check=True,
240+ universal_newlines=True,
241+ )
242+ .stdout.splitlines()[0]
243+ .split(":")[6]
244+ != shell
245+ ):
246+ subprocess.run(["chsh", "-s", shell, user], check=True)
247+
248+
249+def ensure_lp_directories():
250+ for dirpath in oopses_dir(), var_dir():
251+ host.mkdir(dirpath, group=base.user(), perms=0o775)
252+ host.mkdir(secrets_dir(), group=base.user(), perms=0o750)
253+
254+
255+def _get_first_rabbitmq_hostname(rabbitmq):
256+ for conversation in rabbitmq.conversations():
257+ for relation_id in conversation.relation_ids:
258+ for unit in hookenv.related_units(relation_id):
259+ return hookenv.relation_get(
260+ "private-address", unit, relation_id
261+ )
262+
263+
264+def get_service_config():
265+ config = get_payload_config()
266+ config.update(
267+ {
268+ "oopses_dir": oopses_dir(),
269+ "secrets_dir": secrets_dir(),
270+ "var_dir": var_dir(),
271+ }
272+ )
273+
274+ # oops-datedir2amqp is used in crontabs and needs broken-out RabbitMQ
275+ # credentials. This isn't ideal because it doesn't support high
276+ # availability, but we can tolerate that for fallback OOPS publishing
277+ # for now.
278+ if config["rabbitmq_broker_urls"]:
279+ rabbitmq_url = urlparse(config["rabbitmq_broker_urls"].split()[0])
280+ config.update(
281+ {
282+ "rabbitmq_host": rabbitmq_url.hostname,
283+ "rabbitmq_password": rabbitmq_url.password,
284+ "rabbitmq_username": rabbitmq_url.username,
285+ "rabbitmq_vhost": rabbitmq_url.path.lstrip("/"),
286+ }
287+ )
288+ else:
289+ rabbitmq = endpoint_from_flag("rabbitmq.available")
290+ hostname = _get_first_rabbitmq_hostname(rabbitmq)
291+ if hostname is not None:
292+ config.update(
293+ {
294+ "rabbitmq_host": hostname,
295+ "rabbitmq_password": rabbitmq.password(),
296+ "rabbitmq_username": rabbitmq.username(),
297+ "rabbitmq_vhost": rabbitmq.vhost(),
298+ }
299+ )
300+
301+ return config
302+
303+
304+def configure_rsync(config, template, name):
305+ hookenv.log("Writing rsync configuration.")
306+ rsync_path = os.path.join("/etc/rsync-juju.d", name)
307+ if config["log_hosts_allow"]:
308+ templating.render(template, rsync_path, config, perms=0o644)
309+ elif os.path.exists(rsync_path):
310+ os.unlink(rsync_path)
311diff --git a/charm/lp-mailman/metadata.yaml b/charm/lp-mailman/metadata.yaml
312new file mode 100644
313index 0000000..4823b1c
314--- /dev/null
315+++ b/charm/lp-mailman/metadata.yaml
316@@ -0,0 +1,18 @@
317+name: lp-mailman
318+display-name: lp-mailman
319+summary: Launchpad mailing list manager
320+maintainer: Launchpad Developers <launchpad-dev@lists.launchpad.net>
321+description: |
322+ Launchpad is an open source suite of tools that help people and teams
323+ to work together on software projects.
324+
325+ This charm integrates Mailman with Launchpad to operate mailing lists.
326+tags:
327+ # https://juju.is/docs/charm-metadata#heading--charm-store-fields
328+ - network
329+series:
330+ - bionic
331+subordinate: false
332+requires:
333+ rabbitmq:
334+ interface: rabbitmq
335diff --git a/charm/lp-mailman/reactive/lp-mailman.py b/charm/lp-mailman/reactive/lp-mailman.py
336new file mode 100644
337index 0000000..e6230fb
338--- /dev/null
339+++ b/charm/lp-mailman/reactive/lp-mailman.py
340@@ -0,0 +1,171 @@
341+# Copyright 2023 Canonical Ltd. This software is licensed under the
342+# GNU Affero General Public License version 3 (see the file LICENSE).
343+
344+import subprocess
345+
346+from charmhelpers.core import hookenv, host, templating
347+from charms.launchpad.payload import (
348+ config_file_path,
349+ configure_cron,
350+ configure_lazr,
351+)
352+from charms.lp_mailman import (
353+ change_shell,
354+ configure_rsync,
355+ ensure_lp_directories,
356+ get_service_config,
357+)
358+from charms.reactive import (
359+ clear_flag,
360+ endpoint_from_flag,
361+ helpers,
362+ hook,
363+ set_flag,
364+ when,
365+ when_any,
366+ when_none,
367+ when_not,
368+ when_not_all,
369+)
370+from ols import base
371+
372+
373+@when("rabbitmq.connected")
374+def prepare_rabbitmq():
375+ rabbitmq = endpoint_from_flag("rabbitmq.connected")
376+ config = hookenv.config()
377+ rabbitmq.request_access(config["rabbitmq_user"], config["domain"])
378+
379+
380+def get_rabbitmq_uris(rabbitmq):
381+ config = hookenv.config()
382+ if config["rabbitmq_broker_urls"]:
383+ yield from config["rabbitmq_broker_urls"].split()
384+ else:
385+ for conversation in rabbitmq.conversations():
386+ for relation_id in conversation.relation_ids:
387+ for unit in hookenv.related_units(relation_id):
388+ hostname = hookenv.relation_get(
389+ "private-address", unit, relation_id
390+ )
391+ vhost = rabbitmq.vhost()
392+ username = rabbitmq.username()
393+ password = rabbitmq.password()
394+ yield f"amqp://{username}:{password}@{hostname}/{vhost}"
395+
396+
397+@when_any("rabbitmq.available", "config.set.rabbitmq_broker_urls")
398+def rabbitmq_available():
399+ set_flag("lp-mailman.rabbitmq.available")
400+
401+
402+@when_none("rabbitmq.available", "config.set.rabbitmq_broker_urls")
403+def rabbitmq_unavailable():
404+ clear_flag("lp-mailman.rabbitmq.available")
405+
406+
407+def configure_service(config):
408+ hookenv.log("Writing systemd service.")
409+ templating.render(
410+ "lp-mailman.service.j2",
411+ "/lib/systemd/system/lp-mailman.service",
412+ config,
413+ )
414+ subprocess.run(["systemctl", "daemon-reload"], check=True)
415+
416+
417+def configure_logrotate(config):
418+ hookenv.log("Writing logrotate configuration.")
419+ templating.render(
420+ "logrotate.conf.j2", "/etc/logrotate.d/lp-mailman", config, perms=0o644
421+ )
422+
423+
424+def config_files():
425+ files = []
426+ files.append(config_file_path("lp-mailman/mailman-lazr.conf"))
427+ files.append(config_file_path("lp-mailman-secrets-lazr.conf", secret=True))
428+ return files
429+
430+
431+@when("launchpad.payload.configured", "lp-mailman.rabbitmq.available")
432+@when_not("service.configured")
433+def configure():
434+ rabbitmq = endpoint_from_flag("rabbitmq.available")
435+ # Interactive use shouldn't be frequent, but it's still needed
436+ # sometimes, so make it less annoying.
437+ change_shell(base.user(), "/bin/bash")
438+ ensure_lp_directories()
439+ config = get_service_config()
440+ config["rabbitmq_broker_urls"] = sorted(get_rabbitmq_uris(rabbitmq))
441+ configure_lazr(
442+ config, "lp-mailman-lazr.conf", "lp-mailman/mailman-lazr.conf"
443+ )
444+ configure_lazr(
445+ config,
446+ "lp-mailman-secrets-lazr.conf",
447+ "lp-mailman-secrets-lazr.conf",
448+ secret=True,
449+ )
450+ # Normally we'd be careful to ensure that the code tree is owned by root
451+ # while the service runs as a non-root user so that it can't write to
452+ # its own code, but that's currently very difficult to arrange here: the
453+ # build procedure writes to `lib/mailman/`, but it also populates
454+ # `config.mailman.build_var_dir` and the service needs to be able to
455+ # write to that at run-time. We'll just have to put up with the code
456+ # being owned by the service user for now.
457+ subprocess.run(
458+ ["chown", "-R", "%s:", base.user(), base.code_dir()], check=True
459+ )
460+ # Build the tree now that we have the correct configuration in place.
461+ subprocess.run(
462+ [
463+ "make",
464+ "compile",
465+ "PYTHON=%s" % base.python_bin(),
466+ "LPCONFIG=lp-mailman",
467+ ],
468+ cwd=base.code_dir(),
469+ check=True,
470+ )
471+ configure_rsync(config, "lp-mailman-rsync.conf", "010-lp-mailman.conf")
472+ configure_service(config)
473+ configure_logrotate(config)
474+ configure_cron(config, "crontab.j2")
475+
476+ if config["active"]:
477+ if helpers.any_file_changed(
478+ [
479+ base.version_info_path(),
480+ "/lib/systemd/system/lp-mailman.service",
481+ ]
482+ + config_files()
483+ ):
484+ hookenv.log("Config files changed; restarting lp-mailman")
485+ host.service_restart("lp-mailman.service")
486+ host.service_resume("lp-mailman.service")
487+ else:
488+ host.service_pause("lp-mailman.service")
489+
490+ set_flag("service.configured")
491+
492+
493+@when("service.configured")
494+@when_not_all("launchpad.payload.configured", "lp-mailman.rabbitmq.available")
495+def deconfigure():
496+ clear_flag("service.configured")
497+
498+
499+@when("service.configured")
500+def check_is_running():
501+ hookenv.status_set("active", "Ready")
502+
503+
504+@when("config.changed")
505+def config_changed():
506+ clear_flag("service.configured")
507+
508+
509+@hook("{requires:rabbitmq}-relation-changed")
510+def rabbitmq_relation_changed(*args):
511+ clear_flag("service.configured")
512diff --git a/charm/lp-mailman/templates/crontab.j2 b/charm/lp-mailman/templates/crontab.j2
513new file mode 100644
514index 0000000..53b2ed3
515--- /dev/null
516+++ b/charm/lp-mailman/templates/crontab.j2
517@@ -0,0 +1,9 @@
518+TZ=UTC
519+MAILFROM={{ bounce_address }}
520+MAILTO={{ cron_mailto }}
521+LPCONFIG=lp-mailman
522+
523+# Catch up with publishing OOPSes that were temporarily spooled to disk due
524+# to RabbitMQ being unavailable.
525+*/15 * * * * {{ code_dir }}/bin/datedir2amqp --exchange oopses --host {{ rabbitmq_host }} --username {{ rabbitmq_username }} --password {{ rabbitmq_password }} --vhost {{ rabbitmq_vhost }} --repo {{ oopses_dir }} --key ""
526+
527diff --git a/charm/lp-mailman/templates/logrotate.conf.j2 b/charm/lp-mailman/templates/logrotate.conf.j2
528new file mode 100644
529index 0000000..044297e
530--- /dev/null
531+++ b/charm/lp-mailman/templates/logrotate.conf.j2
532@@ -0,0 +1,16 @@
533+{{ logs_dir }}/*.log
534+{
535+ rotate 90
536+ daily
537+ dateext
538+ delaycompress
539+ compress
540+ notifempty
541+ missingok
542+ create 0644 {{ user }} {{ user }}
543+ sharedscripts
544+ postrotate
545+ systemctl restart lp-mailman.service
546+ endscript
547+}
548+
549diff --git a/charm/lp-mailman/templates/lp-mailman-lazr.conf b/charm/lp-mailman/templates/lp-mailman-lazr.conf
550new file mode 100644
551index 0000000..cdccf13
552--- /dev/null
553+++ b/charm/lp-mailman/templates/lp-mailman-lazr.conf
554@@ -0,0 +1,35 @@
555+# Public configuration data. The contents of this file may be freely shared
556+# with developers if needed for debugging.
557+
558+# A schema's sections, keys, and values are automatically inherited, except
559+# for '.optional' sections. Update this config to override key values.
560+# Values are strings, except for numbers that look like ints. The tokens
561+# true, false, and none are treated as True, False, and None.
562+
563+[meta]
564+extends: ../../lib/lp/services/config/schema-lazr.conf
565+
566+[canonical]
567+pid_dir: {{ var_dir }}
568+
569+[error_reports]
570+error_dir: {{ oopses_dir }}
571+oops_prefix: {{ oops_prefix }}
572+
573+[launchpad]
574+config_overlay_dir: {{ secrets_dir }}
575+site_message: {{ site_message }}
576+
577+[mailman]
578+archive_url_template: https://{{ domain_lists }}/$team_name
579+build: True
580+build_host_name: {{ domain_lists }}
581+build_prefix: lib/mailman
582+build_user_group: {{ user }}:{{ user }}
583+build_var_dir: {{ var_dir }}/mailman
584+launch: True
585+list_owner_header_template: https://{{ domain }}/~$team_name
586+list_subscription_headers: https://{{ domain }}/~$team_name
587+subscription_batch_size: 5
588+xmlrpc_url: http://{{ domain_xmlrpc_private }}:{{ port_xmlrpc }}/mailinglists
589+
590diff --git a/charm/lp-mailman/templates/lp-mailman-rsync.conf b/charm/lp-mailman/templates/lp-mailman-rsync.conf
591new file mode 100644
592index 0000000..2921300
593--- /dev/null
594+++ b/charm/lp-mailman/templates/lp-mailman-rsync.conf
595@@ -0,0 +1,8 @@
596+
597+[lp-logs]
598+ path = {{ logs_dir }}
599+ comment = LP Logs
600+ list = false
601+ read only = true
602+ hosts allow = {{ log_hosts_allow }}
603+
604diff --git a/charm/lp-mailman/templates/lp-mailman-secrets-lazr.conf b/charm/lp-mailman/templates/lp-mailman-secrets-lazr.conf
605new file mode 100644
606index 0000000..fbb668f
607--- /dev/null
608+++ b/charm/lp-mailman/templates/lp-mailman-secrets-lazr.conf
609@@ -0,0 +1,19 @@
610+# Secret configuration data. This is stored in an overlay directory, mainly
611+# to avoid accidental information leaks from the public configuration file.
612+# Entries in this file should not be shared with developers, although the
613+# structure of the file is not secret, only configuration values.
614+
615+# A schema's sections, keys, and values are automatically inherited, except
616+# for '.optional' sections. Update this config to override key values.
617+# Values are strings, except for numbers that look like ints. The tokens
618+# true, false, and none are treated as True, False, and None.
619+
620+{% from "macros.j2" import opt -%}
621+
622+[mailman]
623+build_site_list_owner: {{ site_list_owner }}
624+{{- opt("shared_secret", mailman_shared_secret) }}
625+
626+[rabbitmq]
627+broker_urls: {{ rabbitmq_broker_urls|join(" ") }}
628+
629diff --git a/charm/lp-mailman/templates/lp-mailman.service.j2 b/charm/lp-mailman/templates/lp-mailman.service.j2
630new file mode 100644
631index 0000000..869273a
632--- /dev/null
633+++ b/charm/lp-mailman/templates/lp-mailman.service.j2
634@@ -0,0 +1,16 @@
635+[Unit]
636+Description=Launchpad mailing list manager
637+After=network.target
638+ConditionPathExists=!{{ code_dir }}/maintenance.txt
639+
640+[Service]
641+User={{ user }}
642+Group={{ user }}
643+WorkingDirectory={{ code_dir }}
644+Environment=LPCONFIG=lp-mailman
645+ExecStart={{ code_dir }}/bin/run -i ${LPCONFIG}
646+Restart=on-failure
647+
648+[Install]
649+WantedBy=multi-user.target
650+
651diff --git a/lib/lp/services/mailman/monkeypatches/__init__.py b/lib/lp/services/mailman/monkeypatches/__init__.py
652index 0311737..23760b0 100644
653--- a/lib/lp/services/mailman/monkeypatches/__init__.py
654+++ b/lib/lp/services/mailman/monkeypatches/__init__.py
655@@ -9,6 +9,25 @@ import shutil
656 from lazr.config import as_host_port
657
658
659+def write_if_different(path, contents):
660+ """Write `contents` to `path` if they differ from the current contents.
661+
662+ This allows us to monkey-patch Mailman in such a way that changing the
663+ configuration works gracefully in development (where
664+ `lib/mailman/Mailman/` will normally be writeable by the user running
665+ Mailman) while not breaking in Juju-deployed environments (where it
666+ normally won't): we just need to be careful to run `make compile` with
667+ the correct configuration first as a user that can write to the source
668+ tree.
669+ """
670+ if os.path.exists(path):
671+ with open(path) as f:
672+ previous_contents = f.read()
673+ if contents != previous_contents:
674+ with open(path, "w") as f:
675+ f.write(contents)
676+
677+
678 def monkey_patch(mailman_path, config):
679 """Monkey-patch an installed Mailman 2.1 tree.
680
681@@ -49,54 +68,44 @@ def monkey_patch(mailman_path, config):
682 owner_address, owner_password = configure_siteowner(
683 config.mailman.build_site_list_owner)
684 config_path_in = os.path.join(os.path.dirname(__file__), 'mm_cfg.py.in')
685- config_file_in = open(config_path_in)
686- try:
687+ with open(config_path_in) as config_file_in:
688 config_template = config_file_in.read()
689- finally:
690- config_file_in.close()
691 config_path_out = os.path.join(mailman_path, 'Mailman', 'mm_cfg.py')
692- config_file_out = open(config_path_out, 'w')
693- try:
694- print >> config_file_out, config_template % dict(
695- launchpad_top=launchpad_top,
696- smtp_host=host,
697- smtp_port=port,
698- smtp_max_rcpts=config.mailman.smtp_max_rcpts,
699- smtp_max_sesions_per_connection=(
700- config.mailman.smtp_max_sesions_per_connection),
701- xmlrpc_url=config.mailman.xmlrpc_url,
702- xmlrpc_sleeptime=config.mailman.xmlrpc_runner_sleep,
703- xmlrpc_timeout=config.mailman.xmlrpc_timeout,
704- xmlrpc_subscription_batch_size=(
705- config.mailman.subscription_batch_size),
706- site_list_owner=owner_address,
707- list_help_header=config.mailman.list_help_header,
708- list_subscription_headers=(
709- config.mailman.list_subscription_headers),
710- archive_url_template=config.mailman.archive_url_template,
711- list_owner_header_template=(
712- config.mailman.list_owner_header_template),
713- footer=footer,
714- var_dir=config.mailman.build_var_dir,
715- shared_secret=config.mailman.shared_secret,
716- soft_max_size=config.mailman.soft_max_size,
717- hard_max_size=config.mailman.hard_max_size,
718- register_bounces_every=config.mailman.register_bounces_every,
719- )
720- finally:
721- config_file_out.close()
722+ config_path_contents = config_template % dict(
723+ launchpad_top=launchpad_top,
724+ smtp_host=host,
725+ smtp_port=port,
726+ smtp_max_rcpts=config.mailman.smtp_max_rcpts,
727+ smtp_max_sesions_per_connection=(
728+ config.mailman.smtp_max_sesions_per_connection),
729+ xmlrpc_url=config.mailman.xmlrpc_url,
730+ xmlrpc_sleeptime=config.mailman.xmlrpc_runner_sleep,
731+ xmlrpc_timeout=config.mailman.xmlrpc_timeout,
732+ xmlrpc_subscription_batch_size=(
733+ config.mailman.subscription_batch_size),
734+ site_list_owner=owner_address,
735+ list_help_header=config.mailman.list_help_header,
736+ list_subscription_headers=(
737+ config.mailman.list_subscription_headers),
738+ archive_url_template=config.mailman.archive_url_template,
739+ list_owner_header_template=(
740+ config.mailman.list_owner_header_template),
741+ footer=footer,
742+ var_dir=config.mailman.build_var_dir,
743+ shared_secret=config.mailman.shared_secret,
744+ soft_max_size=config.mailman.soft_max_size,
745+ hard_max_size=config.mailman.hard_max_size,
746+ register_bounces_every=config.mailman.register_bounces_every,
747+ )
748+ write_if_different(config_path_out, config_path_contents)
749 # Mailman's qrunner system requires runner modules to live in the
750 # Mailman.Queue package. Set things up so that there's a hook module in
751 # there for the XMLRPCRunner.
752 runner_path = os.path.join(mailman_path,
753 'Mailman', 'Queue', 'XMLRPCRunner.py')
754- runner_file = open(runner_path, 'w')
755- try:
756- print >> runner_file, (
757- 'from lp.services.mailman.monkeypatches.xmlrpcrunner '
758- 'import *')
759- finally:
760- runner_file.close()
761+ write_if_different(
762+ runner_path,
763+ 'from lp.services.mailman.monkeypatches.xmlrpcrunner import *\n')
764 # Install our handler wrapper modules so that Mailman can find them. Most
765 # of the actual code of the handler comes from our monkey patches modules.
766 for mm_name, lp_name in (('LaunchpadMember', 'lphandler'),
767@@ -107,13 +116,9 @@ def monkey_patch(mailman_path, config):
768 ):
769 handler_path = os.path.join(
770 mailman_path, 'Mailman', 'Handlers', mm_name + '.py')
771- handler_file = open(handler_path, 'w')
772- try:
773- package = 'lp.services.mailman.monkeypatches'
774- module = package + '.' + lp_name
775- print >> handler_file, 'from', module, 'import *'
776- finally:
777- handler_file.close()
778+ package = 'lp.services.mailman.monkeypatches'
779+ module = package + '.' + lp_name
780+ write_if_different(handler_path, 'from %s import *\n' % module)
781
782 here = os.path.dirname(__file__)
783 # Install the MHonArc control file.

Subscribers

People subscribed via source and target branches