Merge ~wck0/landscape-charm:lndeng-539-hashid-leader-only into landscape-charm:main
- Git
- lp:~wck0/landscape-charm
- lndeng-539-hashid-leader-only
- Merge into main
Status: | Merged |
---|---|
Merge reported by: | Bill Kronholm |
Merged at revision: | bf0ced80654dab4d270e73fc8970cab08472374a |
Proposed branch: | ~wck0/landscape-charm:lndeng-539-hashid-leader-only |
Merge into: | landscape-charm:main |
Diff against target: |
888 lines (+301/-217) 3 files modified
README.md (+2/-3) src/charm.py (+293/-207) src/haproxy-config.yaml (+6/-7) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kevin Nasto | Approve | ||
Review via email: mp+445573@code.launchpad.net |
Commit message
Only the leader unit should serve hash-id-databases.
Fix bug where package-upload backend might be lost
Description of the change
Bill Kronholm (wck0) wrote : | # |
- 0f290f1... by Bill Kronholm
-
Remove logging warnings. Revert changes to postgres charm.
Bill Kronholm (wck0) wrote : | # |
Updated
Kevin Nasto (silverdrake11) wrote : | # |
Did you run black on the code? It's confusing to see the changes you made vs. formatting changes in the diff. It should be in a separate branch, or at least in a separate commit. Will still approve now though.
Kevin Nasto (silverdrake11) wrote : | # |
In the haproxy-config.yaml why is hashid-databases on 8080, same port as the appserver? Is it meant to be the same service?
Kevin Nasto (silverdrake11) wrote : | # |
Per offline Mattermost thread with @mitchburton, we decided it would be best to install the landscape-hashids deb on first install, but not on upgrade, so please add this otherwise LGTM
- bf0ced8... by Bill Kronholm
-
Only install landscape-hashids on install.
Bill Kronholm (wck0) wrote : | # |
Updated. You can see the most recent changes in src/charm.py and src/haproxy-
Here is a diff for the latest commit:
diff --git a/src/charm.py b/src/charm.py
index 719edb3..e1f2587 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -74,7 +74,6 @@ LANDSCAPE_PACKAGES = (
"landscape
"landscape
"landscape
- "landscape-
)
DEFAULT_SERVICES = (
diff --git a/src/haproxy-
index 9be1ddc..ac0475e 100644
--- a/src/haproxy-
+++ b/src/haproxy-
@@ -55,8 +55,6 @@ ports:
message-server: 8090
api: 9080
package-upload: 9100
- pppa-proxy: 9298
- hashid-database: 8080
server_options:
- check
Preview Diff
1 | diff --git a/README.md b/README.md |
2 | index 86763ce..5832b12 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -69,6 +69,5 @@ When developing the charm, here's a quick way to test out changes as |
6 | they would be deployed by `landscape-scalable`: |
7 | |
8 | ```bash |
9 | -charmcraft pack |
10 | -juju deploy ./bundle.yaml |
11 | -``` |
12 | \ No newline at end of file |
13 | +make build |
14 | +``` |
15 | diff --git a/src/charm.py b/src/charm.py |
16 | index 8665696..e1f2587 100755 |
17 | --- a/src/charm.py |
18 | +++ b/src/charm.py |
19 | @@ -21,31 +21,48 @@ from subprocess import CalledProcessError, check_call |
20 | import yaml |
21 | |
22 | from charms.operator_libs_linux.v0 import apt |
23 | -from charms.operator_libs_linux.v0.apt import ( |
24 | - PackageError, PackageNotFoundError) |
25 | +from charms.operator_libs_linux.v0.apt import PackageError, PackageNotFoundError |
26 | from charms.operator_libs_linux.v0.passwd import group_exists, user_exists |
27 | from charms.operator_libs_linux.v0.systemd import service_reload |
28 | |
29 | from ops.charm import ( |
30 | - ActionEvent, CharmBase, InstallEvent, LeaderElectedEvent, |
31 | - LeaderSettingsChangedEvent, RelationChangedEvent, RelationJoinedEvent, |
32 | - UpdateStatusEvent) |
33 | + ActionEvent, |
34 | + CharmBase, |
35 | + InstallEvent, |
36 | + LeaderElectedEvent, |
37 | + LeaderSettingsChangedEvent, |
38 | + RelationChangedEvent, |
39 | + RelationDepartedEvent, |
40 | + RelationJoinedEvent, |
41 | + UpdateStatusEvent, |
42 | +) |
43 | from ops.framework import StoredState |
44 | from ops.main import main |
45 | from ops.model import ( |
46 | - ActiveStatus, BlockedStatus, Relation, MaintenanceStatus, WaitingStatus) |
47 | + ActiveStatus, |
48 | + BlockedStatus, |
49 | + Relation, |
50 | + MaintenanceStatus, |
51 | + WaitingStatus, |
52 | +) |
53 | |
54 | from settings_files import ( |
55 | - DEFAULT_POSTGRES_PORT, configure_for_deployment_mode, merge_service_conf, |
56 | - prepend_default_settings, update_db_conf, update_default_settings, update_service_conf, |
57 | - write_license_file, write_ssl_cert) |
58 | + DEFAULT_POSTGRES_PORT, |
59 | + configure_for_deployment_mode, |
60 | + merge_service_conf, |
61 | + prepend_default_settings, |
62 | + update_db_conf, |
63 | + update_default_settings, |
64 | + update_service_conf, |
65 | + write_license_file, |
66 | + write_ssl_cert, |
67 | +) |
68 | |
69 | logger = logging.getLogger(__name__) |
70 | |
71 | DEBCONF_SET_SELECTIONS = "/usr/bin/debconf-set-selections" |
72 | DPKG_RECONFIGURE = "/usr/sbin/dpkg-reconfigure" |
73 | -HAPROXY_CONFIG_FILE = os.path.join(os.path.dirname(__file__), |
74 | - "haproxy-config.yaml") |
75 | +HAPROXY_CONFIG_FILE = os.path.join(os.path.dirname(__file__), "haproxy-config.yaml") |
76 | LSCTL = "/usr/bin/lsctl" |
77 | NRPE_D_DIR = "/etc/nagios/nrpe.d" |
78 | POSTFIX_CF = "/etc/postfix/main.cf" |
79 | @@ -57,7 +74,6 @@ LANDSCAPE_PACKAGES = ( |
80 | "landscape-server", |
81 | "landscape-client", |
82 | "landscape-common", |
83 | - "landscape-hashids" |
84 | ) |
85 | |
86 | DEFAULT_SERVICES = ( |
87 | @@ -71,6 +87,7 @@ DEFAULT_SERVICES = ( |
88 | LEADER_SERVICES = ( |
89 | "landscape-package-search", |
90 | "landscape-package-upload", |
91 | + "landscape-hashid-databases", |
92 | ) |
93 | |
94 | OPENID_CONFIG_VALS = ( |
95 | @@ -100,47 +117,59 @@ class LandscapeServerCharm(CharmBase): |
96 | self.framework.observe(self.on.update_status, self._update_status) |
97 | |
98 | # Relations |
99 | - self.framework.observe(self.on.db_relation_joined, |
100 | - self._db_relation_changed) |
101 | - self.framework.observe(self.on.db_relation_changed, |
102 | - self._db_relation_changed) |
103 | - self.framework.observe(self.on.amqp_relation_joined, |
104 | - self._amqp_relation_joined) |
105 | - self.framework.observe(self.on.amqp_relation_changed, |
106 | - self._amqp_relation_changed) |
107 | - self.framework.observe(self.on.website_relation_joined, |
108 | - self._website_relation_joined) |
109 | - self.framework.observe(self.on.website_relation_changed, |
110 | - self._website_relation_changed) |
111 | - self.framework.observe(self.on.nrpe_external_master_relation_joined, |
112 | - self._nrpe_external_master_relation_joined) |
113 | - self.framework.observe(self.on.application_dashboard_relation_joined, |
114 | - self._application_dashboard_relation_joined) |
115 | + self.framework.observe(self.on.db_relation_joined, self._db_relation_changed) |
116 | + self.framework.observe(self.on.db_relation_changed, self._db_relation_changed) |
117 | + self.framework.observe(self.on.amqp_relation_joined, self._amqp_relation_joined) |
118 | + self.framework.observe( |
119 | + self.on.amqp_relation_changed, self._amqp_relation_changed |
120 | + ) |
121 | + self.framework.observe( |
122 | + self.on.website_relation_joined, self._website_relation_joined |
123 | + ) |
124 | + self.framework.observe( |
125 | + self.on.website_relation_changed, self._website_relation_changed |
126 | + ) |
127 | + self.framework.observe( |
128 | + self.on.website_relation_departed, self._website_relation_departed |
129 | + ) |
130 | + self.framework.observe( |
131 | + self.on.nrpe_external_master_relation_joined, |
132 | + self._nrpe_external_master_relation_joined, |
133 | + ) |
134 | + self.framework.observe( |
135 | + self.on.application_dashboard_relation_joined, |
136 | + self._application_dashboard_relation_joined, |
137 | + ) |
138 | |
139 | # Leadership/peering |
140 | self.framework.observe(self.on.leader_elected, self._leader_elected) |
141 | - self.framework.observe(self.on.leader_settings_changed, |
142 | - self._leader_settings_changed) |
143 | - self.framework.observe(self.on.replicas_relation_joined, |
144 | - self._on_replicas_relation_joined) |
145 | - self.framework.observe(self.on.replicas_relation_changed, |
146 | - self._on_replicas_relation_changed) |
147 | + self.framework.observe( |
148 | + self.on.leader_settings_changed, self._leader_settings_changed |
149 | + ) |
150 | + self.framework.observe( |
151 | + self.on.replicas_relation_joined, self._on_replicas_relation_joined |
152 | + ) |
153 | + self.framework.observe( |
154 | + self.on.replicas_relation_changed, self._on_replicas_relation_changed |
155 | + ) |
156 | |
157 | # Actions |
158 | self.framework.observe(self.on.pause_action, self._pause) |
159 | self.framework.observe(self.on.resume_action, self._resume) |
160 | self.framework.observe(self.on.upgrade_action, self._upgrade) |
161 | - self.framework.observe(self.on.migrate_schema_action, |
162 | - self._migrate_schema) |
163 | - self.framework.observe(self.on.hash_id_databases_action, |
164 | - self._hash_id_databases) |
165 | + self.framework.observe(self.on.migrate_schema_action, self._migrate_schema) |
166 | + self.framework.observe( |
167 | + self.on.hash_id_databases_action, self._hash_id_databases |
168 | + ) |
169 | |
170 | # State |
171 | - self._stored.set_default(ready={ |
172 | - "db": False, |
173 | - "amqp": False, |
174 | - "haproxy": False, |
175 | - }) |
176 | + self._stored.set_default( |
177 | + ready={ |
178 | + "db": False, |
179 | + "amqp": False, |
180 | + "haproxy": False, |
181 | + } |
182 | + ) |
183 | self._stored.set_default(leader_ip="") |
184 | self._stored.set_default(running=False) |
185 | self._stored.set_default(paused=False) |
186 | @@ -173,10 +202,8 @@ class LandscapeServerCharm(CharmBase): |
187 | # Write the license file, if it exists. |
188 | license_file = self.model.config.get("license_file") |
189 | if license_file: |
190 | - self.unit.status = MaintenanceStatus( |
191 | - "Writing Landscape license file") |
192 | - write_license_file( |
193 | - license_file, self.landscape_uid, self.root_gid) |
194 | + self.unit.status = MaintenanceStatus("Writing Landscape license file") |
195 | + write_license_file(license_file, self.landscape_uid, self.root_gid) |
196 | self.unit.status = WaitingStatus("Waiting on relations") |
197 | |
198 | smtp_relay_host = self.model.config.get("smtp_relay_host") |
199 | @@ -189,10 +216,12 @@ class LandscapeServerCharm(CharmBase): |
200 | for relation in haproxy_relations: |
201 | self._update_haproxy_connection(relation) |
202 | |
203 | - if any(self.model.config.get(v) for v in OPENID_CONFIG_VALS) \ |
204 | - and any(self.model.config.get(v) for v in OIDC_CONFIG_VALS): |
205 | + if any(self.model.config.get(v) for v in OPENID_CONFIG_VALS) and any( |
206 | + self.model.config.get(v) for v in OIDC_CONFIG_VALS |
207 | + ): |
208 | self.unit.status = BlockedStatus( |
209 | - "OpenID and OIDC configurations are mutually exclusive") |
210 | + "OpenID and OIDC configurations are mutually exclusive" |
211 | + ) |
212 | else: |
213 | self._configure_openid() |
214 | self._configure_oidc() |
215 | @@ -200,11 +229,13 @@ class LandscapeServerCharm(CharmBase): |
216 | # Update root_url, if provided |
217 | root_url = self.model.config.get("root_url") |
218 | if root_url: |
219 | - update_service_conf({ |
220 | - "global": {"root-url": root_url}, |
221 | - "api": {"root-url": root_url}, |
222 | - "package-upload": {"root-url": root_url}, |
223 | - }) |
224 | + update_service_conf( |
225 | + { |
226 | + "global": {"root-url": root_url}, |
227 | + "api": {"root-url": root_url}, |
228 | + "package-upload": {"root-url": root_url}, |
229 | + } |
230 | + ) |
231 | |
232 | config_host = self.model.config.get("db_host") |
233 | schema_password = self.model.config.get("db_schema_password") |
234 | @@ -262,8 +293,7 @@ class LandscapeServerCharm(CharmBase): |
235 | license_file = self.model.config.get("license_file") |
236 | |
237 | if license_file: |
238 | - self.unit.status = MaintenanceStatus( |
239 | - "Writing Landscape license file") |
240 | + self.unit.status = MaintenanceStatus("Writing Landscape license file") |
241 | write_license_file(license_file, self.landscape_uid, self.root_gid) |
242 | |
243 | self.unit.status = ActiveStatus("Unit is ready") |
244 | @@ -283,10 +313,10 @@ class LandscapeServerCharm(CharmBase): |
245 | return |
246 | |
247 | if not all(self._stored.ready.values()): |
248 | - waiting_on = [ |
249 | - rel for rel, ready in self._stored.ready.items() if not ready] |
250 | + waiting_on = [rel for rel, ready in self._stored.ready.items() if not ready] |
251 | self.unit.status = WaitingStatus( |
252 | - "Waiting on relations: {}".format(", ".join(waiting_on))) |
253 | + "Waiting on relations: {}".format(", ".join(waiting_on)) |
254 | + ) |
255 | return |
256 | |
257 | if self._stored.running and not restart_services: |
258 | @@ -309,19 +339,23 @@ class LandscapeServerCharm(CharmBase): |
259 | deployment_mode = self.model.config.get("deployment_mode") |
260 | is_standalone = deployment_mode == "standalone" |
261 | |
262 | - update_default_settings({ |
263 | - "RUN_ALL": "no", |
264 | - "RUN_APISERVER": str(self.model.config["worker_counts"]), |
265 | - "RUN_ASYNC_FRONTEND": "yes", |
266 | - "RUN_JOBHANDLER": "yes", |
267 | - "RUN_APPSERVER": str(self.model.config["worker_counts"]), |
268 | - "RUN_MSGSERVER": str(self.model.config["worker_counts"]), |
269 | - "RUN_PINGSERVER": str(self.model.config["worker_counts"]), |
270 | - "RUN_CRON": "yes" if is_leader else "no", |
271 | - "RUN_PACKAGESEARCH": "yes" if is_leader else "no", |
272 | - "RUN_PACKAGEUPLOADSERVER": "yes" if is_leader and is_standalone else "no", |
273 | - "RUN_PPPA_PROXY": "no", |
274 | - }) |
275 | + update_default_settings( |
276 | + { |
277 | + "RUN_ALL": "no", |
278 | + "RUN_APISERVER": str(self.model.config["worker_counts"]), |
279 | + "RUN_ASYNC_FRONTEND": "yes", |
280 | + "RUN_JOBHANDLER": "yes", |
281 | + "RUN_APPSERVER": str(self.model.config["worker_counts"]), |
282 | + "RUN_MSGSERVER": str(self.model.config["worker_counts"]), |
283 | + "RUN_PINGSERVER": str(self.model.config["worker_counts"]), |
284 | + "RUN_CRON": "yes" if is_leader else "no", |
285 | + "RUN_PACKAGESEARCH": "yes" if is_leader else "no", |
286 | + "RUN_PACKAGEUPLOADSERVER": "yes" |
287 | + if is_leader and is_standalone |
288 | + else "no", |
289 | + "RUN_PPPA_PROXY": "no", |
290 | + } |
291 | + ) |
292 | |
293 | logger.info("Starting services") |
294 | |
295 | @@ -353,7 +387,7 @@ class LandscapeServerCharm(CharmBase): |
296 | |
297 | allowed_units = unit_data["allowed-units"].split() |
298 | if self.unit.name not in allowed_units: |
299 | - logger.info("%s not in allowed_units") |
300 | + logger.info(f"{self.unit.name} not in allowed_units") |
301 | self.unit.status = ActiveStatus("Unit is ready") |
302 | self._update_ready_status() |
303 | return |
304 | @@ -393,8 +427,13 @@ class LandscapeServerCharm(CharmBase): |
305 | else: |
306 | user = unit_data["user"] |
307 | |
308 | - update_db_conf(host=host, port=port, user=user, password=password, |
309 | - schema_password=schema_password) |
310 | + update_db_conf( |
311 | + host=host, |
312 | + port=port, |
313 | + user=user, |
314 | + password=password, |
315 | + schema_password=schema_password, |
316 | + ) |
317 | |
318 | if not self._migrate_schema_bootstrap(): |
319 | return |
320 | @@ -424,10 +463,12 @@ class LandscapeServerCharm(CharmBase): |
321 | self._stored.ready["amqp"] = False |
322 | self.unit.status = MaintenanceStatus("Setting up amqp connection") |
323 | |
324 | - event.relation.data[self.unit].update({ |
325 | - "username": "landscape", |
326 | - "vhost": "landscape", |
327 | - }) |
328 | + event.relation.data[self.unit].update( |
329 | + { |
330 | + "username": "landscape", |
331 | + "vhost": "landscape", |
332 | + } |
333 | + ) |
334 | |
335 | def _amqp_relation_changed(self, event): |
336 | unit_data = event.relation.data[event.unit] |
337 | @@ -442,12 +483,14 @@ class LandscapeServerCharm(CharmBase): |
338 | if isinstance(hostname, list): |
339 | hostname = ",".join(hostname) |
340 | |
341 | - update_service_conf({ |
342 | - "broker": { |
343 | - "host": hostname, |
344 | - "password": password, |
345 | + update_service_conf( |
346 | + { |
347 | + "broker": { |
348 | + "host": hostname, |
349 | + "password": password, |
350 | + } |
351 | } |
352 | - }) |
353 | + ) |
354 | |
355 | self._stored.ready["amqp"] = True |
356 | self.unit.status = ActiveStatus("Unit is ready") |
357 | @@ -460,11 +503,13 @@ class LandscapeServerCharm(CharmBase): |
358 | if not self.model.config.get("root_url"): |
359 | url = f'https://{event.relation.data[event.unit]["public-address"]}/' |
360 | self._stored.default_root_url = url |
361 | - update_service_conf({ |
362 | - "global": {"root-url": url}, |
363 | - "api": {"root-url": url}, |
364 | - "package-upload": {"root-url": url}, |
365 | - }) |
366 | + update_service_conf( |
367 | + { |
368 | + "global": {"root-url": url}, |
369 | + "api": {"root-url": url}, |
370 | + "package-upload": {"root-url": url}, |
371 | + } |
372 | + ) |
373 | |
374 | self._update_ready_status() |
375 | |
376 | @@ -480,7 +525,8 @@ class LandscapeServerCharm(CharmBase): |
377 | if ssl_cert != "DEFAULT" and ssl_key == "": |
378 | # We have a cert but no key, this is an error. |
379 | self.unit.status = BlockedStatus( |
380 | - "`ssl_cert` is specified but `ssl_key` is missing") |
381 | + "`ssl_cert` is specified but `ssl_key` is missing" |
382 | + ) |
383 | return |
384 | |
385 | if ssl_cert != "DEFAULT": |
386 | @@ -490,8 +536,8 @@ class LandscapeServerCharm(CharmBase): |
387 | ssl_cert = b64encode(ssl_cert + b"\n" + ssl_key) |
388 | except binascii.Error: |
389 | self.unit.status = BlockedStatus( |
390 | - "Unable to decode `ssl_cert` or `ssl_key` - must be " |
391 | - "b64-encoded") |
392 | + "Unable to decode `ssl_cert` or `ssl_key` - must be " "b64-encoded" |
393 | + ) |
394 | return |
395 | |
396 | with open(HAPROXY_CONFIG_FILE) as haproxy_config_file: |
397 | @@ -501,68 +547,83 @@ class LandscapeServerCharm(CharmBase): |
398 | https_service = haproxy_config["https_service"] |
399 | https_service["crts"] = [ssl_cert] |
400 | |
401 | - if self.unit.is_leader(): |
402 | - https_service["service_options"].extend( |
403 | - haproxy_config["leader_service_options"]) |
404 | - |
405 | server_ip = relation.data[self.unit]["private-address"] |
406 | unit_name = self.unit.name.replace("/", "-") |
407 | worker_counts = self.model.config["worker_counts"] |
408 | |
409 | (appservers, pingservers, message_servers, api_servers) = [ |
410 | - [( |
411 | - f"landscape-{name}-{unit_name}-{i}", |
412 | - server_ip, |
413 | - haproxy_config["ports"][name] + i, |
414 | - haproxy_config["server_options"], |
415 | - ) for i in range(worker_counts)] |
416 | + [ |
417 | + ( |
418 | + f"landscape-{name}-{unit_name}-{i}", |
419 | + server_ip, |
420 | + haproxy_config["ports"][name] + i, |
421 | + haproxy_config["server_options"], |
422 | + ) |
423 | + for i in range(worker_counts) |
424 | + ] |
425 | for name in ("appserver", "pingserver", "message-server", "api") |
426 | ] |
427 | |
428 | # There should only ever be one package-upload-server service. |
429 | - package_upload_servers = [( |
430 | - f"landscape-package-upload-{unit_name}-0", |
431 | - server_ip, |
432 | - haproxy_config["ports"]["package-upload"], |
433 | - haproxy_config["server_options"], |
434 | - )] |
435 | + package_upload_servers = [ |
436 | + ( |
437 | + f"landscape-package-upload-{unit_name}-0", |
438 | + server_ip, |
439 | + haproxy_config["ports"]["package-upload"], |
440 | + haproxy_config["server_options"], |
441 | + ) |
442 | + ] |
443 | |
444 | http_service["servers"] = appservers |
445 | - http_service["backends"] = [{ |
446 | - "backend_name": "landscape-ping", |
447 | - "servers": pingservers, |
448 | - }] |
449 | + http_service["backends"] = [ |
450 | + { |
451 | + "backend_name": "landscape-ping", |
452 | + "servers": pingservers, |
453 | + } |
454 | + ] |
455 | https_service["servers"] = appservers |
456 | - https_service["backends"] = [{ |
457 | - "backend_name": "landscape-message", |
458 | - "servers": message_servers, |
459 | - }, { |
460 | - "backend_name": "landscape-api", |
461 | - "servers": api_servers, |
462 | - }] |
463 | - |
464 | - if self.unit.is_leader(): |
465 | - https_service["backends"].append({ |
466 | + https_service["backends"] = [ |
467 | + { |
468 | + "backend_name": "landscape-message", |
469 | + "servers": message_servers, |
470 | + }, |
471 | + { |
472 | + "backend_name": "landscape-api", |
473 | + "servers": api_servers, |
474 | + }, |
475 | + # Only the leader should have servers for the landscape-package-upload |
476 | + # and landscape-hashid-databases backends. However, when the leader |
477 | + # is lost, haproxy will fail as the service options will reference |
478 | + # a (no longer) existing backend. To prevent that, all units should |
479 | + # declare all backends, even if a unit should not have any servers on |
480 | + # a specific backend. |
481 | + { |
482 | "backend_name": "landscape-package-upload", |
483 | - "servers": package_upload_servers, |
484 | - }) |
485 | + "servers": package_upload_servers if self.unit.is_leader() else [], |
486 | + }, |
487 | + { |
488 | + "backend_name": "landscape-hashid-databases", |
489 | + "servers": appservers if self.unit.is_leader() else [], |
490 | + }, |
491 | + ] |
492 | |
493 | error_files_location = haproxy_config["error_files"]["location"] |
494 | error_files = [] |
495 | for code, filename in haproxy_config["error_files"]["files"].items(): |
496 | error_file_path = os.path.join(error_files_location, filename) |
497 | with open(error_file_path, "rb") as error_file: |
498 | - error_files.append({ |
499 | - "http_status": code, |
500 | - "content": b64encode(error_file.read()) |
501 | - }) |
502 | + error_files.append( |
503 | + {"http_status": code, "content": b64encode(error_file.read())} |
504 | + ) |
505 | |
506 | http_service["error_files"] = error_files |
507 | https_service["error_files"] = error_files |
508 | |
509 | - relation.data[self.unit].update({ |
510 | - "services": yaml.safe_dump([http_service, https_service]) |
511 | - }) |
512 | + relation.data[self.unit].update( |
513 | + { |
514 | + "services": yaml.safe_dump([http_service, https_service]), |
515 | + } |
516 | + ) |
517 | |
518 | self._stored.ready["haproxy"] = True |
519 | |
520 | @@ -591,13 +652,14 @@ class LandscapeServerCharm(CharmBase): |
521 | write_ssl_cert(haproxy_ssl_cert) |
522 | |
523 | self.unit.status = ActiveStatus("Unit is ready") |
524 | - |
525 | self._update_haproxy_connection(event.relation) |
526 | |
527 | self._update_ready_status() |
528 | |
529 | - def _nrpe_external_master_relation_joined( |
530 | - self, event: RelationJoinedEvent) -> None: |
531 | + def _website_relation_departed(self, event: RelationDepartedEvent) -> None: |
532 | + event.relation.data[self.unit].update({"services": ""}) |
533 | + |
534 | + def _nrpe_external_master_relation_joined(self, event: RelationJoinedEvent) -> None: |
535 | self._update_nrpe_checks(event.relation) |
536 | |
537 | def _update_nrpe_checks(self, relation: Relation): |
538 | @@ -613,16 +675,16 @@ class LandscapeServerCharm(CharmBase): |
539 | monitors = { |
540 | "monitors": { |
541 | "remote": { |
542 | - "nrpe": { |
543 | - s: {"command": f"check_{s}"} for s in services_to_add |
544 | - }, |
545 | + "nrpe": {s: {"command": f"check_{s}"} for s in services_to_add}, |
546 | }, |
547 | }, |
548 | } |
549 | |
550 | - relation.data[self.unit].update({ |
551 | - "monitors": yaml.safe_dump(monitors), |
552 | - }) |
553 | + relation.data[self.unit].update( |
554 | + { |
555 | + "monitors": yaml.safe_dump(monitors), |
556 | + } |
557 | + ) |
558 | |
559 | if not os.path.exists(NRPE_D_DIR): |
560 | logger.debug("NRPE directories not ready") |
561 | @@ -636,12 +698,14 @@ class LandscapeServerCharm(CharmBase): |
562 | continue |
563 | |
564 | with open(cfg_filename, "w") as cfg_fp: |
565 | - cfg_fp.write(f"""# check {service} |
566 | + cfg_fp.write( |
567 | + f"""# check {service} |
568 | # The following header was added by the landscape-server charm |
569 | # Modifying it will affect nagios monitoring and alerting |
570 | # servicegroups: juju |
571 | command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service} |
572 | -""") |
573 | +""" |
574 | + ) |
575 | |
576 | for service in services_to_remove: |
577 | service_cfg = service.replace("-", "_") |
578 | @@ -662,7 +726,8 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
579 | |
580 | if not root_url: |
581 | root_url = "https://" + str( |
582 | - self.model.get_binding(event.relation).network.bind_address) |
583 | + self.model.get_binding(event.relation).network.bind_address |
584 | + ) |
585 | |
586 | site_name = self.model.config.get("site_name") |
587 | if site_name: |
588 | @@ -679,13 +744,15 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
589 | else: |
590 | icon_data = None |
591 | |
592 | - event.relation.data[self.app].update({ |
593 | - "name": "Landscape", |
594 | - "url": root_url, |
595 | - "subtitle": subtitle, |
596 | - "group": group, |
597 | - "icon": icon_data, |
598 | - }) |
599 | + event.relation.data[self.app].update( |
600 | + { |
601 | + "name": "Landscape", |
602 | + "url": root_url, |
603 | + "subtitle": subtitle, |
604 | + "group": group, |
605 | + "icon": icon_data, |
606 | + } |
607 | + ) |
608 | |
609 | def _leader_elected(self, event: LeaderElectedEvent) -> None: |
610 | # Just because we received this event does not mean we are |
611 | @@ -698,16 +765,17 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
612 | ip = str(self.model.get_binding(peer_relation).network.bind_address) |
613 | peer_relation.data[self.app].update({"leader-ip": ip}) |
614 | |
615 | - update_service_conf({ |
616 | - "package-search": { |
617 | - "host": "localhost", |
618 | - }, |
619 | - }) |
620 | + update_service_conf( |
621 | + { |
622 | + "package-search": { |
623 | + "host": "localhost", |
624 | + }, |
625 | + } |
626 | + ) |
627 | |
628 | self._leader_changed() |
629 | |
630 | - def _leader_settings_changed( |
631 | - self, event: LeaderSettingsChangedEvent) -> None: |
632 | + def _leader_settings_changed(self, event: LeaderSettingsChangedEvent) -> None: |
633 | # Just because we received this event does not mean we are |
634 | # guaranteed to be a follower by the time we process it. See |
635 | # https://juju.is/docs/sdk/leader-elected-event |
636 | @@ -717,11 +785,13 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
637 | leader_ip = peer_relation.data[self.app].get("leader-ip") |
638 | |
639 | if leader_ip: |
640 | - update_service_conf({ |
641 | - "package-search": { |
642 | - "host": leader_ip, |
643 | - }, |
644 | - }) |
645 | + update_service_conf( |
646 | + { |
647 | + "package-search": { |
648 | + "host": leader_ip, |
649 | + }, |
650 | + } |
651 | + ) |
652 | |
653 | self._leader_changed() |
654 | |
655 | @@ -736,9 +806,10 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
656 | for relation in nrpe_relations: |
657 | self._update_nrpe_checks(relation) |
658 | |
659 | - haproxy_relations = self.model.relations.get("website", []) |
660 | - for relation in haproxy_relations: |
661 | - self._update_haproxy_connection(relation) |
662 | + if self.unit.is_leader(): |
663 | + haproxy_relations = self.model.relations.get("website", []) |
664 | + for relation in haproxy_relations: |
665 | + self._update_haproxy_connection(relation) |
666 | |
667 | self._update_ready_status(restart_services=True) |
668 | |
669 | @@ -749,13 +820,17 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
670 | |
671 | event.relation.data[self.unit].update({"unit-data": self.unit.name}) |
672 | |
673 | - def _on_replicas_relation_changed( |
674 | - self, event: RelationChangedEvent) -> None: |
675 | + def _on_replicas_relation_changed(self, event: RelationChangedEvent) -> None: |
676 | leader_ip_value = event.relation.data[self.app].get("leader-ip") |
677 | |
678 | if leader_ip_value and leader_ip_value != self._stored.leader_ip: |
679 | self._stored.leader_ip = leader_ip_value |
680 | |
681 | + if self.unit.is_leader(): |
682 | + haproxy_relations = self.model.relations.get("website", []) |
683 | + for relation in haproxy_relations: |
684 | + self._update_haproxy_connection(relation) |
685 | + |
686 | def _configure_smtp(self, relay_host: str) -> None: |
687 | |
688 | # Rewrite postfix config. |
689 | @@ -789,27 +864,32 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
690 | none_count = oidc_vals.count(None) |
691 | |
692 | if none_count == 0: |
693 | - update_service_conf({ |
694 | - "landscape": { |
695 | - "oidc-issuer": oidc_issuer, |
696 | - "oidc-client-id": oidc_client_id, |
697 | - "oidc-client-secret": oidc_client_secret, |
698 | - "oidc-logout-url": oidc_logout_url, |
699 | - }, |
700 | - }) |
701 | + update_service_conf( |
702 | + { |
703 | + "landscape": { |
704 | + "oidc-issuer": oidc_issuer, |
705 | + "oidc-client-id": oidc_client_id, |
706 | + "oidc-client-secret": oidc_client_secret, |
707 | + "oidc-logout-url": oidc_logout_url, |
708 | + }, |
709 | + } |
710 | + ) |
711 | elif none_count == 1 and oidc_logout_url is None: |
712 | # Only the logout url is optional. |
713 | - update_service_conf({ |
714 | - "landscape": { |
715 | - "oidc-issuer": oidc_issuer, |
716 | - "oidc-client-id": oidc_client_id, |
717 | - "oidc-client-secret": oidc_client_secret, |
718 | - }, |
719 | - }) |
720 | + update_service_conf( |
721 | + { |
722 | + "landscape": { |
723 | + "oidc-issuer": oidc_issuer, |
724 | + "oidc-client-id": oidc_client_id, |
725 | + "oidc-client-secret": oidc_client_secret, |
726 | + }, |
727 | + } |
728 | + ) |
729 | elif none_count < 4: |
730 | self.unit.status = BlockedStatus( |
731 | "OIDC connect config requires at least 'oidc_issuer', " |
732 | - "'oidc_client_id', and 'oidc_client_secret' values") |
733 | + "'oidc_client_id', and 'oidc_client_secret' values" |
734 | + ) |
735 | return |
736 | |
737 | self.unit.status = WaitingStatus("Waiting on relations") |
738 | @@ -821,17 +901,20 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
739 | openid_logout_url = self.model.config.get("openid_logout_url") |
740 | |
741 | if openid_provider_url and openid_logout_url: |
742 | - update_service_conf({ |
743 | - "landscape": { |
744 | - "openid-provider-url": openid_provider_url, |
745 | - "openid-logout-url": openid_logout_url, |
746 | - }, |
747 | - }) |
748 | + update_service_conf( |
749 | + { |
750 | + "landscape": { |
751 | + "openid-provider-url": openid_provider_url, |
752 | + "openid-logout-url": openid_logout_url, |
753 | + }, |
754 | + } |
755 | + ) |
756 | self.unit.status = WaitingStatus("Waiting on relations") |
757 | elif openid_provider_url or openid_logout_url: |
758 | self.unit.status = BlockedStatus( |
759 | "OpenID configuration requires both 'openid_provider_url' and " |
760 | - "'openid_logout_url'") |
761 | + "'openid_logout_url'" |
762 | + ) |
763 | |
764 | def _bootstrap_account(self): |
765 | """If admin account details are provided, create admin""" |
766 | @@ -895,8 +978,7 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
767 | try: |
768 | check_call([LSCTL, "stop"]) |
769 | except CalledProcessError as e: |
770 | - logger.error("Stopping services failed with return code %d", |
771 | - e.returncode) |
772 | + logger.error("Stopping services failed with return code %d", e.returncode) |
773 | self.unit.status = BlockedStatus("Failed to stop services") |
774 | event.fail("Failed to stop services") |
775 | else: |
776 | @@ -908,14 +990,12 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
777 | self.unit.status = MaintenanceStatus("Starting services") |
778 | event.log("Starting services") |
779 | |
780 | - start_result = subprocess.run([LSCTL, "start"], capture_output=True, |
781 | - text=True) |
782 | + start_result = subprocess.run([LSCTL, "start"], capture_output=True, text=True) |
783 | |
784 | try: |
785 | check_call([LSCTL, "status"]) |
786 | except CalledProcessError as e: |
787 | - logger.error("Starting services failed with return code %d", |
788 | - e.returncode) |
789 | + logger.error("Starting services failed with return code %d", e.returncode) |
790 | logger.error("Failed to start services: %s", start_result.stdout) |
791 | self.unit.status = MaintenanceStatus("Stopping services") |
792 | subprocess.run([LSCTL, "stop"]) |
793 | @@ -929,8 +1009,10 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
794 | |
795 | def _upgrade(self, event: ActionEvent) -> None: |
796 | if self._stored.running: |
797 | - event.fail("Cannot upgrade while running. Please run action " |
798 | - "'pause' prior to upgrade") |
799 | + event.fail( |
800 | + "Cannot upgrade while running. Please run action " |
801 | + "'pause' prior to upgrade" |
802 | + ) |
803 | return |
804 | |
805 | prev_status = self.unit.status |
806 | @@ -947,7 +1029,9 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
807 | installed = apt.DebianPackage.from_installed_package(package) |
808 | event.log(f"Upgraded to {installed.version}...") |
809 | except PackageNotFoundError as e: |
810 | - logger.error(f"Could not upgrade package {package}. Reason: {e.message}") |
811 | + logger.error( |
812 | + f"Could not upgrade package {package}. Reason: {e.message}" |
813 | + ) |
814 | event.fail(f"Could not upgrade package {package}. Reason: {e.message}") |
815 | self.unit.status = BlockedStatus("Failed to upgrade packages") |
816 | return |
817 | @@ -956,8 +1040,10 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
818 | |
819 | def _migrate_schema(self, event: ActionEvent) -> None: |
820 | if self._stored.running: |
821 | - event.fail("Cannot migrate schema while running. Please run action" |
822 | - " 'pause' prior to migration") |
823 | + event.fail( |
824 | + "Cannot migrate schema while running. Please run action" |
825 | + " 'pause' prior to migration" |
826 | + ) |
827 | return |
828 | |
829 | prev_status = self.unit.status |
830 | @@ -967,10 +1053,8 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
831 | try: |
832 | subprocess.run([SCHEMA_SCRIPT], check=True, text=True) |
833 | except CalledProcessError as e: |
834 | - logger.error("Schema migration failed with error code %s", |
835 | - e.returncode) |
836 | - event.fail("Schema migration failed with error code %s", |
837 | - e.returncode) |
838 | + logger.error("Schema migration failed with error code %s", e.returncode) |
839 | + event.fail("Schema migration failed with error code %s", e.returncode) |
840 | self.unit.status = BlockedStatus("Failed schema migration") |
841 | else: |
842 | self.unit.status = prev_status |
843 | @@ -981,7 +1065,9 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/check_systemd.py {service |
844 | event.log("Running hash_id_databases") |
845 | |
846 | try: |
847 | - subprocess.run(["sudo", "-u", "landscape", HASH_ID_DATABASES], check=True, text=True) |
848 | + subprocess.run( |
849 | + ["sudo", "-u", "landscape", HASH_ID_DATABASES], check=True, text=True |
850 | + ) |
851 | except CalledProcessError as e: |
852 | logger.error("Hashing ID databases failed with error code %s", e.returncode) |
853 | event.fail("Hashing ID databases failed with error code %s", e.returncode) |
854 | diff --git a/src/haproxy-config.yaml b/src/haproxy-config.yaml |
855 | index e986f53..ac0475e 100644 |
856 | --- a/src/haproxy-config.yaml |
857 | +++ b/src/haproxy-config.yaml |
858 | @@ -34,11 +34,11 @@ https_service: |
859 | - use_backend landscape-message if attachment |
860 | - use_backend landscape-api if api |
861 | - use_backend landscape-ping if ping |
862 | - |
863 | -leader_service_options: |
864 | - - acl package-upload path_beg -i /upload |
865 | - - use_backend landscape-package-upload if package-upload |
866 | - - reqrep ^([^\ ]*)\ /upload/(.*) \1\ /\2 |
867 | + - acl hashids path_beg -i /hash-id-databases |
868 | + - use_backend landscape-hashid-databases if hashids |
869 | + - acl package-upload path_beg -i /upload |
870 | + - use_backend landscape-package-upload if package-upload |
871 | + - reqrep ^([^\ ]*)\ /upload/(.*) \1\ /\2 |
872 | |
873 | error_files: |
874 | location: /opt/canonical/landscape/canonical/landscape/offline |
875 | @@ -55,11 +55,10 @@ ports: |
876 | message-server: 8090 |
877 | api: 9080 |
878 | package-upload: 9100 |
879 | - pppa-proxy: 9298 |
880 | |
881 | server_options: |
882 | - check |
883 | - inter 5000 |
884 | - rise 2 |
885 | - fall 5 |
886 | - - maxconn 50 |
887 | \ No newline at end of file |
888 | + - maxconn 50 |
Manual Testing Instructions:
0. Grab the new code.
1. From the repo directory, build the project:
make build
2. Once you are able, add some new landscape-server units:
juju add-unit landscape-server -n 4 # or however many extras you want; add at least 2 more units
3. Once you can, monitor the haproxy log file on the haproxy/0 unit for http requests to the /upload and /hash-id-databases endpoints. Do this however you like, but here is one way: haproxy. log | grep -e database -e upload
juju ssh haproxy/0
tail -f /var/log/
4. Meanwhile, wait for the landscape-server units to settle. Once they do, identify the landscape-server leader and the IP address for haproxy:
watch --color juju status --color --relations
5. Make repeated requests to the /upload and /hash-id-databases endpoints. Observe that only the leader unit responds to these requests: /IP.ADDR. OF.HAPROXY/ upload /IP.ADDR. OF.HAPROXY/ hash-id- databases
curl --insecure https:/
curl --insecure https:/
6. Make repeated requests to the / endpoint (or any other). Observe that haproxy balances the load across all landscape-server units. /IP.ADDR. OF.HAPROXY/
curl --insecure https:/
7. Remove the landscape-server leader unit:
juju remove-unit landscape-server/n # replace n with the leader's number
8. Once the new leader is elected, repeat 5 and 6 above, observing the requests in step 5 go only to the new leader. If the leader has not yet settled, these requests should yield 503 NOSRV from haproxy.
9. Repeat steps 7 and 8 ad nauseam.
10. Clean up:
make clean