Merge ~cjohnston/charm-grafana:lp1877796-ldap-integration into charm-grafana:master
- Git
- lp:~cjohnston/charm-grafana
- lp1877796-ldap-integration
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | James Troup | ||||
Approved revision: | 366e4235fa718b38a59e0712209b90668c93ab1f | ||||
Merged at revision: | 3ec06b8337777d566e34552715c097dab91b4ebc | ||||
Proposed branch: | ~cjohnston/charm-grafana:lp1877796-ldap-integration | ||||
Merge into: | charm-grafana:master | ||||
Diff against target: |
1070 lines (+637/-41) 18 files modified
src/README.md (+33/-0) src/actions/do-upgrade (+5/-1) src/config.yaml (+59/-0) src/lib/charms/layer/grafana.py (+117/-9) src/reactive/grafana.py (+119/-7) src/templates/grafana.ini.j2 (+5/-0) src/templates/ldap.toml.j2 (+58/-0) src/tests/functional/tests/bundles/base.yaml (+4/-1) src/tests/functional/tests/bundles/overlays/focal-self-signed-cert.yaml.j2 (+2/-0) src/tests/functional/tests/bundles/overlays/focal-snap-self-signed-cert.yaml.j2 (+2/-0) src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 (+2/-0) src/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 (+3/-1) src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 (+2/-0) src/tests/functional/tests/bundles/overlays/focal.yaml.j2 (+3/-1) src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 (+2/-0) src/tests/functional/tests/bundles/overlays/xenial.yaml.j2 (+2/-0) src/tests/functional/tests/test_grafana.py (+218/-21) src/tests/unit/requirements.txt (+1/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
🤖 prod-jenkaas-bootstack (community) | continuous-integration | Needs Fixing | |
James Troup (community) | Needs Fixing | ||
BootStack Reviewers | Pending | ||
Review via email: mp+415017@code.launchpad.net |
Commit message
Add support for LDAP authentication.
Description of the change
Add support for LDAP authentication.
This required some refactoring of other pieces that weren't working properly in order to make them work properly as well as the new code working properly.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:d5625a88859
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:26b5f641f56
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:23c2eb59491
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:054d0375521
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
James Troup (elmo) wrote : | # |
See inline comment
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:11d7c28c951
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:283eff37925
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
James Troup (elmo) wrote : | # |
Thanks Chris for working on this - it looks great and is awesome functionality to have. I've reviewed it and I'm happy, my only comment is that you have merge conflicts in the tox.ini - if you fix that, it'll be +1 from me.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 3ec06b8337777d5
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:366e4235fa7
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/src/README.md b/src/README.md |
2 | index 2b3e49c..4e2d009 100644 |
3 | --- a/src/README.md |
4 | +++ b/src/README.md |
5 | @@ -129,6 +129,39 @@ If not all URL paths are behind the reverse proxy auth, |
6 | and `anonymous=true` is set, those paths will be accessible (view only) to |
7 | non-authenticated users. |
8 | |
9 | +## LDAP auth |
10 | + |
11 | +Grafana can be deployed to utilize LDAP as an authentication mechanism. To |
12 | +enable LDAP authentication, all `ldap_*` config options |
13 | +(except `ldap_config_flags`) must be set. |
14 | + |
15 | +Additional config settings as defined in the upstream |
16 | +[LDAP Authentication documentation](https://grafana.com/docs/grafana/latest/auth/ldap/) |
17 | +may be set using the `ldap_config_flags` option. |
18 | + |
19 | +Group mappings for permissions should be defined with the following: |
20 | + |
21 | +``` |
22 | +ldap_config_flags: "{ |
23 | + group_mapping_admin: 'cn=grafanaadmins,dc=test,dc=com', |
24 | + group_mapping_editor: 'cn=grafanaeditors,dc=test,dc=com', |
25 | + group_mapping_viewer: 'cn=grafanaviewers,dc=test,dc=com', |
26 | +}" |
27 | +``` |
28 | + |
29 | +Server attributes should be defined as `attribute_<attribute>`. Example: |
30 | + |
31 | + |
32 | +``` |
33 | +ldap_config_flags: "{ |
34 | + "attribute_member_of: memberOf," |
35 | + "attribute_email: email," |
36 | + "attribute_name: givenname," |
37 | + "attribute_surname: sn," |
38 | + "attribute_username: cn," |
39 | +}" |
40 | +``` |
41 | + |
42 | ## Development |
43 | |
44 | After modifying code, you must assemble the charm: |
45 | diff --git a/src/actions/do-upgrade b/src/actions/do-upgrade |
46 | index edb0eb9..662fba8 100755 |
47 | --- a/src/actions/do-upgrade |
48 | +++ b/src/actions/do-upgrade |
49 | @@ -11,7 +11,10 @@ from charmhelpers.core.hookenv import ( |
50 | status_set, |
51 | ) |
52 | from charms.layer import snap |
53 | -from charms.reactive import remove_state |
54 | +from charms.reactive import ( |
55 | + remove_state, |
56 | + clear_flag, |
57 | +) |
58 | |
59 | |
60 | sys.path.append("lib") |
61 | @@ -35,6 +38,7 @@ def snap_upgrade(channel): |
62 | ) |
63 | function_set({"result": "Upgraded, channel {}".format(channel)}) |
64 | status_set("active", "Ready") |
65 | + clear_flag("snap_channel.change.upgrade") |
66 | remove_state("grafana.change-block") |
67 | elif result == ChangeStatus.UNCHANGED: |
68 | function_fail("Nothing to upgrade") |
69 | diff --git a/src/config.yaml b/src/config.yaml |
70 | index ed84bc2..3198705 100644 |
71 | --- a/src/config.yaml |
72 | +++ b/src/config.yaml |
73 | @@ -85,6 +85,65 @@ options: |
74 | default: False |
75 | type: boolean |
76 | description: Whether to enable default auth.proxy config, defaults to False. |
77 | + ldap_auth: |
78 | + default: False |
79 | + type: boolean |
80 | + description: Whether to enable LDAP authentication, defaults to False. |
81 | + ldap_server: |
82 | + type: string |
83 | + default: "" |
84 | + description: | |
85 | + LDAP server URL for Grafana LDAP identity backend. |
86 | + |
87 | + Examples: |
88 | + ldap://10.10.10.10/ |
89 | + ldaps://10.10.10.10/ |
90 | + ldap://example.com:389/ |
91 | + ldap://active-directory-host.com:3268/ |
92 | + ldaps://active-directory-host.com:3269/ |
93 | + |
94 | + An ldap:// URL will result in mandatory StartTLS usage if the |
95 | + charm's ldap_server_ca option has been specified. |
96 | + ldap_server_ca: |
97 | + type: string |
98 | + default: !!null "" |
99 | + description: | |
100 | + This option controls which certificate (or a chain) will be used to connect |
101 | + to the LDAP server over TLS. Certificate contents should either be used |
102 | + directly or included via include-file:// |
103 | + ldap_user: |
104 | + type: string |
105 | + default: "" |
106 | + description: | |
107 | + Username (Distinguished Name) used to bind to LDAP identity server. |
108 | + . |
109 | + Example: cn=admin,dc=test,dc=com |
110 | + ldap_password: |
111 | + type: string |
112 | + default: "" |
113 | + description: Password of the LDAP identity server. |
114 | + ldap_base_dn: |
115 | + type: string |
116 | + default: "" |
117 | + description: Comma separated list of base dns to be used by Grafana. |
118 | + ldap_config_flags: |
119 | + type: string |
120 | + default: "" |
121 | + description: | |
122 | + Additional LDAP configuration options. |
123 | + For simple configurations use a comma separated string of key=value pairs. |
124 | + "ssl_skip_verify=false, group_search_filter_user_attribute='uid'" |
125 | + For more complex configurations use a json like string with double quotes |
126 | + and braces around all the options and single quotes around complex values. |
127 | + "{ssl_skip_verify: false, |
128 | + group_mapping_admin: 'cn=grafanaadmins,dc=test,dc=com', |
129 | + group_mapping_editor: 'cn=grafanaeditors,dc=test,dc=com', |
130 | + group_mapping_viewer: 'cn=grafanaviewers,dc=test,dc=com'}" |
131 | + |
132 | + See the README for more details. |
133 | + |
134 | + Note: The explicitly defined ldap_* charm config options are required |
135 | + and cannot be set via `ldap_config_flags`. |
136 | datasources: |
137 | default: "" |
138 | type: string |
139 | diff --git a/src/lib/charms/layer/grafana.py b/src/lib/charms/layer/grafana.py |
140 | index 10135be..9c0fb16 100644 |
141 | --- a/src/lib/charms/layer/grafana.py |
142 | +++ b/src/lib/charms/layer/grafana.py |
143 | @@ -3,11 +3,14 @@ |
144 | import base64 |
145 | import enum |
146 | import json |
147 | +import os |
148 | import re |
149 | import subprocess |
150 | import tempfile |
151 | +from urllib.parse import urlparse |
152 | |
153 | -from charmhelpers.core import unitdata |
154 | +from charmhelpers.contrib.openstack.utils import config_flags_parser |
155 | +from charmhelpers.core import host, unitdata |
156 | from charmhelpers.core.hookenv import ( |
157 | cached, |
158 | config, |
159 | @@ -15,9 +18,15 @@ from charmhelpers.core.hookenv import ( |
160 | model_name, |
161 | status_set, |
162 | ) |
163 | +from charmhelpers.core.templating import render |
164 | + |
165 | |
166 | from charms.layer import snap |
167 | -from charms.reactive.flags import is_flag_set |
168 | +from charms.reactive.flags import ( |
169 | + clear_flag, |
170 | + is_flag_set, |
171 | + set_flag, |
172 | +) |
173 | |
174 | import requests |
175 | |
176 | @@ -28,6 +37,7 @@ import requests |
177 | # https://git.launchpad.net/ubuntu/+source/python-certifi/tree/debian/patches/0001-Use-Debian-provided-etc-ssl-certs-ca-certificates.cr.patch |
178 | SYSTEM_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt" |
179 | CA_CERT_PATH = "/var/snap/grafana/common/ssl/ca-certificates.crt" |
180 | +LDAP_TOML_TMPL = "ldap.toml.j2" |
181 | |
182 | |
183 | class ChangeStatus(enum.Enum): |
184 | @@ -311,13 +321,13 @@ def check_snap_channel(snap_name, channel): |
185 | 'The "latest" track is not supported; revert the channel config ' |
186 | "change to unblock" |
187 | ) |
188 | - status_set("blocked", msg) |
189 | log(msg) |
190 | + set_flag("snap_channel.change.latest") |
191 | return ChangeStatus.LATEST |
192 | if new_ver == -1: |
193 | msg = "Invalid snap channel: {}".format(channel) |
194 | - status_set("blocked", msg) |
195 | log(msg) |
196 | + set_flag("snap_channel.change.invald") |
197 | return ChangeStatus.INVALID |
198 | if installed_ver == new_ver and not is_refresh_available: |
199 | status_set("active", "Ready") |
200 | @@ -331,17 +341,15 @@ def check_snap_channel(snap_name, channel): |
201 | status_set("active", "Refresh complete") |
202 | return ChangeStatus.UNCHANGED |
203 | if installed_ver < new_ver: |
204 | - msg = ( |
205 | - "PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION: see {} for more info" |
206 | - ).format(VERSION_CHANGE_URL) |
207 | - status_set("blocked", msg) |
208 | + msg = "PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION" |
209 | log(msg) |
210 | + set_flag("snap_channel.change.upgrade") |
211 | return ChangeStatus.UPGRADE |
212 | msg = ( |
213 | "PACKAGE DOWNGRADES ARE NOT SUPPORTED BY THIS CHARM - to clear " |
214 | "this message, set config 'snap_channel' to '{}/stable'" |
215 | ).format(installed_ver) |
216 | - status_set("blocked", msg) |
217 | + set_flag("snap_channel.change.downgrade") |
218 | log(msg) |
219 | return ChangeStatus.DOWNGRADE |
220 | |
221 | @@ -379,3 +387,103 @@ def get_instance_name(): |
222 | return config("instance_name") |
223 | else: |
224 | return "127.0.0.1" |
225 | + |
226 | + |
227 | +def get_ldap_configs_changed(config): |
228 | + """Check to see if any LDAP config flags have changed. If yes, configure LDAP.""" |
229 | + ldap_cfgs = [k for k in config if k.startswith("ldap_")] |
230 | + for ldap_cfg in ldap_cfgs: |
231 | + if config.changed(ldap_cfg): |
232 | + clear_flag("grafana.ldap.configured") |
233 | + |
234 | + |
235 | +def check_ldap_config_valid(config): |
236 | + """Determine whether sufficient configuration has been provided.""" |
237 | + required_config = { |
238 | + "ldap_server": config["ldap_server"], |
239 | + "ldap_user": config["ldap_user"], |
240 | + "ldap_password": config["ldap_password"], |
241 | + "ldap_base_dn": config["ldap_base_dn"], |
242 | + } |
243 | + return all(required_config.values()) |
244 | + |
245 | + |
246 | +def render_ldap_ca(ldap_server_ca, ldap_ca_path): |
247 | + """Render crt file for LDAP server.""" |
248 | + host.write_file( |
249 | + ldap_ca_path, |
250 | + ldap_server_ca, |
251 | + owner="root", |
252 | + group="root", |
253 | + perms=0o644, |
254 | + ) |
255 | + |
256 | + |
257 | +def render_ldap_config(config, ldap_toml_path, ldap_ca_path=None): |
258 | + """Validate and render the ldap config file.""" |
259 | + clear_flag("ldap.config.incomplete") |
260 | + log("Setting up LDAP auth...") |
261 | + parsed_url = urlparse(config["ldap_server"]) |
262 | + ldap_options, group_mappings, server_attributes = parse_ldap_config_flags( |
263 | + config["ldap_config_flags"] |
264 | + ) |
265 | + settings = { |
266 | + "config": config, |
267 | + "host": parsed_url.hostname, |
268 | + "port": parsed_url.port if parsed_url.port else "389", |
269 | + "ldap_options": ldap_options, |
270 | + "group_mappings": group_mappings, |
271 | + "server_attributes": server_attributes, |
272 | + } |
273 | + if parsed_url.scheme == "ldaps" or ldap_ca_path: |
274 | + settings["ssl"] = "true" |
275 | + if ldap_ca_path: |
276 | + render_ldap_ca(config["ldap_server_ca"], ldap_ca_path) |
277 | + settings["ldap_ca_path"] = ldap_ca_path |
278 | + render( |
279 | + source=LDAP_TOML_TMPL, |
280 | + target=ldap_toml_path, |
281 | + context=settings, |
282 | + owner="grafana", |
283 | + group="grafana", |
284 | + perms=0o600, |
285 | + ) |
286 | + status_set("active", "Completed configuring LDAP authentication") |
287 | + clear_flag("grafana.configured") |
288 | + |
289 | + |
290 | +def parse_ldap_config_flags(ldap_config_flags): |
291 | + """Parse through the ldap_config_flags.""" |
292 | + ldap_options = {} |
293 | + group_mappings = {} |
294 | + server_attributes = {} |
295 | + if ldap_config_flags: |
296 | + parsed_config_flags = config_flags_parser(ldap_config_flags) |
297 | + for k, v in parsed_config_flags.items(): |
298 | + if k.startswith("group_mappings_"): |
299 | + group_mappings.update({k: v}) |
300 | + elif k.startswith("attribute_"): |
301 | + new_k = k.replace("attribute_", "") |
302 | + server_attributes.update({new_k: v}) |
303 | + else: |
304 | + ldap_options.update({k: v}) |
305 | + return ldap_options, group_mappings, server_attributes |
306 | + |
307 | + |
308 | +def remove_ldap_authentication(ldap_toml_path, ldap_ca_path): |
309 | + """Remove LDAP config files if they exist and restart Grafana if needed.""" |
310 | + restart_needed = False |
311 | + try: |
312 | + os.remove(ldap_toml_path) |
313 | + clear_flag("grafana.configured") |
314 | + restart_needed = True |
315 | + except FileNotFoundError: |
316 | + pass |
317 | + try: |
318 | + os.remove(ldap_ca_path) |
319 | + # Is the clear flag actually needed? |
320 | + clear_flag("grafana.configured") |
321 | + except FileNotFoundError: |
322 | + pass |
323 | + |
324 | + return restart_needed |
325 | diff --git a/src/reactive/grafana.py b/src/reactive/grafana.py |
326 | index 9672653..dd70008 100644 |
327 | --- a/src/reactive/grafana.py |
328 | +++ b/src/reactive/grafana.py |
329 | @@ -96,6 +96,7 @@ from charms.layer import snap, tls_client |
330 | from charms.layer.grafana import ( |
331 | CA_CERT_PATH, |
332 | ChangeStatus, |
333 | + check_ldap_config_valid, |
334 | check_snap_channel, |
335 | config_defined_ssl_ca, |
336 | config_defined_ssl_cert, |
337 | @@ -106,12 +107,18 @@ from charms.layer.grafana import ( |
338 | get_deb_package_version, |
339 | get_installed_package_version, |
340 | get_instance_name, |
341 | + get_ldap_configs_changed, |
342 | get_protocol, |
343 | import_dashboard, |
344 | + parse_snap_major_version, |
345 | + remove_ldap_authentication, |
346 | + render_ldap_config, |
347 | ) |
348 | from charms.reactive import ( |
349 | + clear_flag, |
350 | hook, |
351 | remove_state, |
352 | + set_flag, |
353 | set_state, |
354 | when, |
355 | when_any, |
356 | @@ -155,6 +162,14 @@ GRAFANA_INI = { |
357 | "snap": "{}/conf/grafana.ini".format(SNAP_DATA), |
358 | "apt": "/etc/grafana/grafana.ini", |
359 | } |
360 | +LDAP_TOML_PATH = { |
361 | + "snap": "{}/conf/ldap.toml".format(SNAP_DATA), |
362 | + "apt": "/etc/grafana/ldap.toml", |
363 | +} |
364 | +LDAP_CA_PATH = { |
365 | + "snap": "{}/ldap_ca.crt".format(SNAP_DATA), |
366 | + "apt": "/usr/share/ca-certificates/ldap_ca.crt", |
367 | +} |
368 | GRAFANA_INI_TMPL = "grafana.ini.j2" |
369 | GRAFANA_DEPS = ["libfontconfig1"] |
370 | DASHBOARDS_BACKUP_CRON = "/etc/cron.d/juju-dashboards-backup" |
371 | @@ -198,11 +213,9 @@ def install_packages(): |
372 | if not kv.get("install_method", False): |
373 | kv.set("install_method", source) |
374 | elif kv.get("install_method") != source: |
375 | - hookenv.status_set( |
376 | - "blocked", |
377 | - "Install method changes are not supported, " |
378 | - "revert install_method option to previous value", |
379 | - ) |
380 | + set_flag("grafana.change-block") |
381 | + set_flag("install_method.changed") |
382 | + set_final_status() |
383 | return |
384 | |
385 | if source == "apt": |
386 | @@ -259,7 +272,10 @@ def install_packages(): |
387 | set_state("grafana.installed") |
388 | hookenv.status_set("blocked", "Missing relations: grafana-source") |
389 | else: |
390 | - hookenv.status_set("blocked", "Unsupported install_method") |
391 | + set_flag("install_method.unsupported") |
392 | + return |
393 | + clear_flag("install_method.changed") |
394 | + clear_flag("install_method.unsupported") |
395 | |
396 | |
397 | def data_path(): |
398 | @@ -326,12 +342,19 @@ def upgrade_charm(): |
399 | def config_changed(): |
400 | """Run config-changed hook.""" |
401 | config = hookenv.config() |
402 | + if config.changed("install_method"): |
403 | + clear_flag("grafana.installed") |
404 | + clear_flag("grafana.change-block") |
405 | if ( |
406 | config.changed("snap_channel") |
407 | and config["snap_channel"] |
408 | and not config.changed("install_method") |
409 | and config["install_method"] == "snap" |
410 | ): |
411 | + clear_flag("snap_channel.change.latest") |
412 | + clear_flag("snap_channel.change.invalid") |
413 | + clear_flag("snap_channel.change.downgrade") |
414 | + clear_flag("snap_channel.change.upgrade") |
415 | # NOTE(aluria): install_method change (apt -> snap, and viceversa) |
416 | # is not supported. However, snap channel change support was missing |
417 | if snap.is_installed(SNAP_NAME): |
418 | @@ -367,6 +390,7 @@ def config_changed(): |
419 | if config.changed("site_name"): |
420 | application_dashboard_relation_changed() |
421 | remove_state("grafana.nagios-setup.completed") |
422 | + get_ldap_configs_changed(config) |
423 | |
424 | |
425 | def check_ports(new_port): |
426 | @@ -511,6 +535,8 @@ def setup_grafana(): |
427 | "cert_file": CERT_PATH[config["install_method"]], |
428 | "cert_key": CERT_KEY_PATH[config["install_method"]], |
429 | } |
430 | + if is_flag_set("grafana.ldap.configured"): |
431 | + settings["ldap_toml_path"] = LDAP_TOML_PATH[source] |
432 | |
433 | smtp_auth = config.get("smtp_auth", False) |
434 | if smtp_auth and len(smtp_auth.split(":")) == 2: |
435 | @@ -608,13 +634,15 @@ def restart_grafana(): |
436 | |
437 | svcname = SVCNAME[source] |
438 | grafana_ini = GRAFANA_INI[source] |
439 | + ldap_toml = LDAP_TOML_PATH[source] |
440 | + configs_changed = any_file_changed([grafana_ini, ldap_toml]) |
441 | |
442 | if not host.service_running(svcname): |
443 | msg = "Starting {}".format(svcname) |
444 | hookenv.status_set("maintenance", msg) |
445 | hookenv.log(msg) |
446 | host.service_start(svcname) |
447 | - elif any_file_changed([grafana_ini]) or is_state("config.changed.install_plugins"): |
448 | + elif configs_changed or is_state("config.changed.install_plugins"): |
449 | msg = "Restarting {}".format(svcname) |
450 | hookenv.log(msg) |
451 | hookenv.status_set("maintenance", msg) |
452 | @@ -1409,6 +1437,38 @@ def import_dashboards(dashboards): |
453 | kv.set("dash_digests", dashboard_digests) |
454 | |
455 | |
456 | +@when("grafana.started") |
457 | +@when_not("grafana.ldap.configured") |
458 | +@when_not("grafana.change-block") |
459 | +def configure_ldap_authentication(): |
460 | + """Configure LDAP authentication.""" |
461 | + config = hookenv.config() |
462 | + source = get_install_source() |
463 | + if not source: |
464 | + return |
465 | + ldap_toml_path = LDAP_TOML_PATH[source] |
466 | + ldap_ca_path = LDAP_CA_PATH[source] |
467 | + if config.get("ldap_auth"): |
468 | + if not check_ldap_config_valid(config): |
469 | + hookenv.log("Missing required config options for configuring LDAP.") |
470 | + set_flag("ldap.config.incomplete") |
471 | + restart_needed = remove_ldap_authentication(ldap_toml_path, ldap_ca_path) |
472 | + if restart_needed: |
473 | + setup_grafana() |
474 | + restart_grafana() |
475 | + set_final_status() |
476 | + else: |
477 | + if config.get("ldap_server_ca") and config.changed("ldap_server_ca"): |
478 | + render_ldap_config(config, ldap_toml_path, ldap_ca_path) |
479 | + else: |
480 | + render_ldap_config(config, ldap_toml_path) |
481 | + set_flag("grafana.ldap.configured") |
482 | + else: |
483 | + if is_flag_set("ldap.config.incomplete"): |
484 | + clear_flag("ldap.config.incomplete") |
485 | + remove_ldap_authentication(ldap_toml_path, ldap_ca_path) |
486 | + |
487 | + |
488 | @when("certificates.available") |
489 | @when_not("grafana.certificates.configured") |
490 | @when_not("grafana.certificates.requested") |
491 | @@ -1438,6 +1498,7 @@ def tls_request_certificate(tls): |
492 | |
493 | @when("tls_client.certs.changed") |
494 | @when("grafana.certificates.requested") |
495 | +@when_not("grafana.change-block") |
496 | def tls_certificate_changed(): |
497 | """Request to reconfigure grafana after the certificates were written.""" |
498 | _ensure_certs_permissions() |
499 | @@ -1533,3 +1594,54 @@ def _uninstall_cert_key(): |
500 | hookenv.log("Certificate key {0} removed.".format(key_path), level=hookenv.INFO) |
501 | except FileNotFoundError: |
502 | pass |
503 | + |
504 | + |
505 | +@when("grafana.configured") |
506 | +def set_final_status(): |
507 | + """Set the final status of the charm as we leave hook execution.""" |
508 | + config = hookenv.config() |
509 | + |
510 | + if is_flag_set("install_method.changed"): |
511 | + hookenv.status_set( |
512 | + "blocked", |
513 | + "Install method changes are not supported, " |
514 | + "revert install_method option to previous value", |
515 | + ) |
516 | + return |
517 | + |
518 | + if is_flag_set("install_method.unsupported"): |
519 | + hookenv.status_set("blocked", "Unsupported install_method") |
520 | + return |
521 | + |
522 | + if is_flag_set("ldap.config.incomplete"): |
523 | + hookenv.status_set("blocked", "LDAP configuration incomplete") |
524 | + return |
525 | + |
526 | + if is_flag_set("snap_channel.change.latest"): |
527 | + msg = ( |
528 | + 'The "latest" track is not supported; revert the channel config ' |
529 | + "change to unblock" |
530 | + ) |
531 | + hookenv.status_set("blocked", msg) |
532 | + return |
533 | + |
534 | + if is_flag_set("snap_channel.change.invalid"): |
535 | + msg = "Invalid snap channel: {}".format(config["snap_channel"]) |
536 | + hookenv.status_set("blocked", msg) |
537 | + return |
538 | + |
539 | + if is_flag_set("snap_channel.change.upgrade"): |
540 | + msg = "PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION" |
541 | + hookenv.status_set("blocked", msg) |
542 | + return |
543 | + |
544 | + if is_flag_set("snap_channel.change.downgrade"): |
545 | + installed_ver = parse_snap_major_version(snap.get_installed_channel(SNAP_NAME)) |
546 | + msg = ( |
547 | + "PACKAGE DOWNGRADES ARE NOT SUPPORTED BY THIS CHARM - to clear " |
548 | + "this message, set config 'snap_channel' to '{}/stable'" |
549 | + ).format(installed_ver) |
550 | + hookenv.status_set("blocked", msg) |
551 | + return |
552 | + |
553 | + hookenv.status_set("active", "Ready") |
554 | diff --git a/src/templates/grafana.ini.j2 b/src/templates/grafana.ini.j2 |
555 | index aed60b2..4d13235 100644 |
556 | --- a/src/templates/grafana.ini.j2 |
557 | +++ b/src/templates/grafana.ini.j2 |
558 | @@ -200,8 +200,13 @@ auto_sign_up = true |
559 | |
560 | #################################### Auth LDAP ########################## |
561 | [auth.ldap] |
562 | +{% if config['ldap_auth'] and ldap_toml_path -%} |
563 | +enabled = true |
564 | +config_file = {{ ldap_toml_path }} |
565 | +{% else -%} |
566 | ;enabled = false |
567 | ;config_file = /etc/grafana/ldap.toml |
568 | +{% endif %} |
569 | |
570 | #################################### SMTP / Emailing ########################## |
571 | [smtp] |
572 | diff --git a/src/templates/ldap.toml.j2 b/src/templates/ldap.toml.j2 |
573 | new file mode 100644 |
574 | index 0000000..bbf0d27 |
575 | --- /dev/null |
576 | +++ b/src/templates/ldap.toml.j2 |
577 | @@ -0,0 +1,58 @@ |
578 | +[[servers]] |
579 | +host = "{{ host }}" |
580 | +port = {{ port }} |
581 | +bind_dn = "{{ config.ldap_user }}" |
582 | +bind_password = "{{ config.ldap_password }}" |
583 | +{% if ssl or config.ldap_server_ca -%} |
584 | +use_ssl = true |
585 | +{% if config.ldap_server_ca -%} |
586 | +root_ca_cert = {{ ldap_ca_path }} |
587 | +{% endif -%} |
588 | +{% else -%} |
589 | +use_ssl = false |
590 | +{% endif -%} |
591 | +search_base_dns = ["{{ config.ldap_base_dn }}"] |
592 | + |
593 | +{% if 'search_filter' not in ldap_options -%} |
594 | +search_filter = "(cn=%s)" |
595 | +{% endif -%} |
596 | +{% if ldap_options -%} |
597 | +{% for key, value in ldap_options.items() -%} |
598 | +{% if key == 'group_search_base_dns' -%} |
599 | +group_search_base_dns = ["{{ value }}"] |
600 | +{% else -%} |
601 | +{{ key }} = "{{ value }}" |
602 | +{% endif -%} |
603 | +{% endfor -%} |
604 | +{% endif -%} |
605 | + |
606 | +{% if server_attributes -%} |
607 | +[servers.attributes] |
608 | +{% for key, value in server_attributes.items() -%} |
609 | +{{ key }} = "{{ value }}" |
610 | +{% endfor -%} |
611 | +{% endif -%} |
612 | + |
613 | +{% if group_mappings -%} |
614 | +{% for key, value in group_mappings.items() -%} |
615 | +{% if key == "group_mappings_admin" -%} |
616 | + |
617 | +[[servers.group_mappings]] |
618 | +group_dn = "{{ value }}" |
619 | +org_role = "Admin" |
620 | +grafana_admin = true |
621 | +{% endif -%} |
622 | + |
623 | +{% if key == "group_mappings_editor" -%} |
624 | +[[servers.group_mappings]] |
625 | +group_dn = "{{ value }}" |
626 | +org_role = "Editor" |
627 | +{% endif -%} |
628 | + |
629 | +{% if key == "group_mappings_viewer" -%} |
630 | +[[servers.group_mappings]] |
631 | +group_dn = "{{ value }}" |
632 | +org_role = "Viewer" |
633 | +{% endif -%} |
634 | +{% endfor -%} |
635 | +{% endif -%} |
636 | diff --git a/src/tests/functional/tests/bundles/base.yaml b/src/tests/functional/tests/bundles/base.yaml |
637 | index 6b4b218..47354fa 100644 |
638 | --- a/src/tests/functional/tests/bundles/base.yaml |
639 | +++ b/src/tests/functional/tests/bundles/base.yaml |
640 | @@ -53,7 +53,10 @@ applications: |
641 | charm: cs:~llama-charmers-next/prometheus-ceph-exporter |
642 | num_units: 1 |
643 | prometheus-libvirt-exporter: |
644 | - charm: cs:~llama-charmers-next/prometheus-libvirt-exporter |
645 | + charm: cs:~llama-charmers/prometheus-libvirt-exporter |
646 | + ldap-server: |
647 | + charm: cs:~openstack-charmers/ldap-test-fixture |
648 | + num_units: 1 |
649 | |
650 | relations: |
651 | - - keystone:shared-db |
652 | diff --git a/src/tests/functional/tests/bundles/overlays/focal-self-signed-cert.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-self-signed-cert.yaml.j2 |
653 | index c31d2db..cbe4f51 100644 |
654 | --- a/src/tests/functional/tests/bundles/overlays/focal-self-signed-cert.yaml.j2 |
655 | +++ b/src/tests/functional/tests/bundles/overlays/focal-self-signed-cert.yaml.j2 |
656 | @@ -23,3 +23,5 @@ applications: |
657 | series: bionic |
658 | nagios: |
659 | series: bionic |
660 | + ldap-server: |
661 | + series: bionic |
662 | diff --git a/src/tests/functional/tests/bundles/overlays/focal-snap-self-signed-cert.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-snap-self-signed-cert.yaml.j2 |
663 | index 623d230..fafffe0 100644 |
664 | --- a/src/tests/functional/tests/bundles/overlays/focal-snap-self-signed-cert.yaml.j2 |
665 | +++ b/src/tests/functional/tests/bundles/overlays/focal-snap-self-signed-cert.yaml.j2 |
666 | @@ -25,3 +25,5 @@ applications: |
667 | series: bionic |
668 | nagios: |
669 | series: bionic |
670 | + ldap-server: |
671 | + series: bionic |
672 | diff --git a/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 |
673 | index 259f3c5..e0b8218 100644 |
674 | --- a/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 |
675 | +++ b/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 |
676 | @@ -25,6 +25,8 @@ applications: |
677 | series: bionic |
678 | nagios: |
679 | series: bionic |
680 | + ldap-server: |
681 | + series: bionic |
682 | easyrsa: |
683 | charm: cs:~containers/easyrsa |
684 | num_units: 1 |
685 | diff --git a/src/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 |
686 | index 9200330..fafffe0 100644 |
687 | --- a/src/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 |
688 | +++ b/src/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 |
689 | @@ -24,4 +24,6 @@ applications: |
690 | prometheus-libvirt-exporter: |
691 | series: bionic |
692 | nagios: |
693 | - series: bionic |
694 | \ No newline at end of file |
695 | + series: bionic |
696 | + ldap-server: |
697 | + series: bionic |
698 | diff --git a/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 |
699 | index b6283dc..46465c3 100644 |
700 | --- a/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 |
701 | +++ b/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 |
702 | @@ -24,6 +24,8 @@ applications: |
703 | series: bionic |
704 | nagios: |
705 | series: bionic |
706 | + ldap-server: |
707 | + series: bionic |
708 | easyrsa: |
709 | charm: cs:~containers/easyrsa |
710 | num_units: 1 |
711 | diff --git a/src/tests/functional/tests/bundles/overlays/focal.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal.yaml.j2 |
712 | index c2a7e8b..139cc4d 100644 |
713 | --- a/src/tests/functional/tests/bundles/overlays/focal.yaml.j2 |
714 | +++ b/src/tests/functional/tests/bundles/overlays/focal.yaml.j2 |
715 | @@ -23,4 +23,6 @@ applications: |
716 | prometheus-libvirt-exporter: |
717 | series: bionic |
718 | nagios: |
719 | - series: bionic |
720 | \ No newline at end of file |
721 | + series: bionic |
722 | + ldap-server: |
723 | + series: bionic |
724 | diff --git a/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 |
725 | index 86f7ebf..1c1e894 100644 |
726 | --- a/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 |
727 | +++ b/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 |
728 | @@ -8,5 +8,7 @@ applications: |
729 | series: bionic |
730 | charm: cs:~containers/easyrsa |
731 | num_units: 1 |
732 | + ldap-server: |
733 | + series: bionic |
734 | relations: |
735 | - [ grafana:certificates, easyrsa ] |
736 | diff --git a/src/tests/functional/tests/bundles/overlays/xenial.yaml.j2 b/src/tests/functional/tests/bundles/overlays/xenial.yaml.j2 |
737 | index 648f471..b3dd868 100644 |
738 | --- a/src/tests/functional/tests/bundles/overlays/xenial.yaml.j2 |
739 | +++ b/src/tests/functional/tests/bundles/overlays/xenial.yaml.j2 |
740 | @@ -3,3 +3,5 @@ applications: |
741 | grafana: |
742 | options: |
743 | install_method: apt |
744 | + ldap-server: |
745 | + series: bionic |
746 | diff --git a/src/tests/functional/tests/test_grafana.py b/src/tests/functional/tests/test_grafana.py |
747 | index eb49a09..30dbe9e 100644 |
748 | --- a/src/tests/functional/tests/test_grafana.py |
749 | +++ b/src/tests/functional/tests/test_grafana.py |
750 | @@ -1,4 +1,4 @@ |
751 | -"""Encapsulate prometheus-openstack-exporter testing.""" |
752 | +"""Encapsulate Grafana testing.""" |
753 | import base64 |
754 | import json |
755 | import logging |
756 | @@ -16,10 +16,21 @@ TEST_TIMEOUT = 600 |
757 | DEFAULT_API_PORT = "3000" |
758 | DEFAULT_API_URL = "/" |
759 | DEFAULT_BACKUP_DIRECTORY = "/srv/backups" |
760 | +SNAP_NAME = "grafana" |
761 | +SNAP_DATA = "/var/snap/{}/current".format(SNAP_NAME) |
762 | +SNAP_COMMON_DATA = "/var/snap/{}/common/data".format(SNAP_NAME) |
763 | +LDAP_TOML = { |
764 | + "snap": "{}/conf/ldap.toml".format(SNAP_DATA), |
765 | + "apt": "/etc/grafana/ldap.toml", |
766 | +} |
767 | +DATA_DIR = { |
768 | + "snap": SNAP_COMMON_DATA, |
769 | + "apt": "/var/lib/grafana", |
770 | +} |
771 | |
772 | |
773 | class BaseGrafanaTest(unittest.TestCase): |
774 | - """Base for Prometheus-openstack-exporter charm tests. |
775 | + """Base for Grafana charm tests. |
776 | |
777 | - Get the CA from easyrsa (when available) and write it on disk. |
778 | """ |
779 | @@ -32,6 +43,9 @@ class BaseGrafanaTest(unittest.TestCase): |
780 | super(BaseGrafanaTest, cls).setUpClass() |
781 | cls.model_name = model.get_juju_model() |
782 | cls.application_name = "grafana" |
783 | + cls.install_method = model.get_application_config(cls.application_name)[ |
784 | + "install_method" |
785 | + ].get("value") |
786 | cls.lead_unit_name = model.get_lead_unit_name( |
787 | cls.application_name, model_name=cls.model_name |
788 | ) |
789 | @@ -107,15 +121,8 @@ class BaseGrafanaTest(unittest.TestCase): |
790 | timeout = time.time() + TEST_TIMEOUT |
791 | dashboards = [] |
792 | while True: |
793 | - req = requests.get( |
794 | - "{protocol}://{host}:{port}" |
795 | - "/api/search?dashboardIds".format( |
796 | - protocol=self.get_protocol(), |
797 | - host=self.grafana_ip, |
798 | - port=DEFAULT_API_PORT, |
799 | - ), |
800 | - auth=("admin", self.admin_password), |
801 | - verify=self.ca_path, |
802 | + req = self.query_grafana_website( |
803 | + "admin", self.admin_password, "api/search?dashboardIds" |
804 | ) |
805 | self.assertEqual(req.status_code, 200) |
806 | dashboards = json.loads(req.text) |
807 | @@ -124,6 +131,24 @@ class BaseGrafanaTest(unittest.TestCase): |
808 | time.sleep(30) |
809 | return dashboards |
810 | |
811 | + def query_grafana_website(self, user, password, url_path=None): |
812 | + """Load a url.""" |
813 | + if url_path is None: |
814 | + url = "{protocol}://{host}:{port}/".format( |
815 | + protocol=self.get_protocol(), |
816 | + host=self.grafana_ip, |
817 | + port=DEFAULT_API_PORT, |
818 | + ) |
819 | + else: |
820 | + url = "{protocol}://{host}:{port}/{path}".format( |
821 | + protocol=self.get_protocol(), |
822 | + host=self.grafana_ip, |
823 | + port=DEFAULT_API_PORT, |
824 | + path=url_path, |
825 | + ) |
826 | + req = requests.get(url, auth=(user, password), verify=self.ca_path) |
827 | + return req |
828 | + |
829 | def remote_sed(self, unit, file, expr): |
830 | """Run sed on remote unit.""" |
831 | cmd = ["sed", "-i", "-e", expr, file] |
832 | @@ -224,6 +249,32 @@ class CharmOperationTest(BaseGrafanaTest): |
833 | content = result.get("Stdout") |
834 | self.assertIn(expected_nrpe_check, content) |
835 | |
836 | + def test_03_change_install_method(self): |
837 | + """Test changing install_method results in a blocked state.""" |
838 | + revert = "" |
839 | + if self.install_method == "apt": |
840 | + model.set_application_config( |
841 | + self.application_name, |
842 | + {"install_method": "snap"}, |
843 | + ) |
844 | + revert = "apt" |
845 | + else: |
846 | + model.set_application_config( |
847 | + self.application_name, |
848 | + {"install_method": "apt"}, |
849 | + ) |
850 | + revert = "snap" |
851 | + model.block_until_unit_wl_status(self.lead_unit_name, "blocked") |
852 | + model.block_until_all_units_idle() |
853 | + status_message = self.get_unit_status(self.lead_unit_name) |
854 | + self.assertIn("Install method changes are not support", status_message) |
855 | + model.set_application_config( |
856 | + self.application_name, |
857 | + {"install_method": revert}, |
858 | + ) |
859 | + model.block_until_unit_wl_status(self.lead_unit_name, "active") |
860 | + model.block_until_all_units_idle() |
861 | + |
862 | def test_10_grafana_imported_dashboards(self): |
863 | """Check that Grafana dashboards expected are there.""" |
864 | dashboards = self.get_and_check_dashboards(lambda dashes: len(dashes) >= 4) |
865 | @@ -284,15 +335,7 @@ class CharmOperationTest(BaseGrafanaTest): |
866 | ) |
867 | self.assertEqual(action.data["results"]["Code"], "0") |
868 | time.sleep(30) # Dirty hack to overcome race condition |
869 | - req = requests.get( |
870 | - "{protocol}://{host}:{port}/api/org/".format( |
871 | - protocol=self.get_protocol(), |
872 | - host=self.grafana_ip, |
873 | - port=DEFAULT_API_PORT, |
874 | - ), |
875 | - auth=("foouser", "sikkrit"), |
876 | - verify=self.ca_path, |
877 | - ) |
878 | + req = self.query_grafana_website("foouser", "sikkrit", "api/org/") |
879 | self.assertEqual(req.status_code, 200) |
880 | self.assertIn("name", req.json()) |
881 | |
882 | @@ -337,6 +380,9 @@ class CharmOperationTest(BaseGrafanaTest): |
883 | ) |
884 | self.assertIn(port, crontab["Stdout"]) |
885 | self.verify_iterative_backups() |
886 | + # Reset the port so that later tests work without issue |
887 | + model.reset_application_config(self.application_name, ["port"]) |
888 | + model.block_until_all_units_idle() |
889 | |
890 | def test_15_grafana_datasource_updates(self): |
891 | """Change the port on the associated datasource. |
892 | @@ -347,10 +393,159 @@ class CharmOperationTest(BaseGrafanaTest): |
893 | As per LP #1893320 |
894 | """ |
895 | datasource_application = "prometheus" |
896 | - port_config = {"web-listen-port": "1234"} |
897 | + new_port = "1234" |
898 | + port_config = {"web-listen-port": new_port} |
899 | model.set_application_config(datasource_application, port_config) |
900 | model.block_until_all_units_idle() |
901 | |
902 | + # It has been seen that this test may complete due to a slight gap in |
903 | + # time between prometheus going idle and grafana going executing for |
904 | + # this change. Therefore we should verify that the port is now |
905 | + # configured as expected prior to moving on. |
906 | + db_path = "{}/grafana.db".format(DATA_DIR[self.install_method]) |
907 | + py = """python3 -c 'import sqlite3; output = """ |
908 | + conn = """sqlite3.connect("{}", timeout=30)""".format(db_path) |
909 | + query = "SELECT * FROM data_source;" |
910 | + final = ".fetchall(); print(output)" |
911 | + cmd = """{} {}.cursor().execute("{}"){}'""".format(py, conn, query, final) |
912 | + # count = 0 |
913 | + timeout = time.time() + TEST_TIMEOUT |
914 | + while time.time() < timeout: |
915 | + result = model.run_on_unit(self.lead_unit_name, cmd) |
916 | + if new_port in result["Stdout"]: |
917 | + return |
918 | + logging.info("Port not updated yet.. Retrying in 30s.") |
919 | + time.sleep(30) |
920 | + # The port did not change in the allowed time, fail the test |
921 | + self.fail("The port did not change as expected. \n" "Result: {}".format(result)) |
922 | + |
923 | + def test_20_ldap_not_configured(self): |
924 | + """Ensure that LDAP authentication is not configured when not set.""" |
925 | + config_file = LDAP_TOML[self.install_method] |
926 | + expected_output = "cat: {}: No such file or directory".format(config_file) |
927 | + ldap_cfg = model.run_on_unit( |
928 | + self.lead_unit_name, |
929 | + "cat %s" % config_file, |
930 | + ) |
931 | + self.assertIn(expected_output, ldap_cfg["Stderr"]) |
932 | + |
933 | + def test_21_incomplete_ldap_config_raises_blocked(self): |
934 | + """Incomplete LDAP configuration results in a blocked state.""" |
935 | + model.set_application_config( |
936 | + self.application_name, |
937 | + { |
938 | + "ldap_auth": "true", |
939 | + "ldap_password": "crapper", |
940 | + }, |
941 | + ) |
942 | + model.block_until_unit_wl_status(self.lead_unit_name, "blocked") |
943 | + model.block_until_all_units_idle() |
944 | + status_message = self.get_unit_status(self.lead_unit_name) |
945 | + self.assertIn("LDAP configuration incomplete", status_message) |
946 | + |
947 | + def test_22_configure_ldap_authentication(self): |
948 | + """Configure LDAP authentication and validate that a user can login.""" |
949 | + expected_group_mappings = "[[servers.group_mappings]]" |
950 | + config_file = LDAP_TOML[self.install_method] |
951 | + ldap_server_ip = model.get_app_ips("ldap-server")[0] |
952 | + ldap_config_flags = ( |
953 | + "{" |
954 | + "attribute_member_of: memberOf," |
955 | + "attribute_email: email," |
956 | + "attribute_name: givenname," |
957 | + "attribute_surname: sn," |
958 | + "attribute_username: cn," |
959 | + "}" |
960 | + ) |
961 | + model.set_application_config( |
962 | + self.application_name, |
963 | + { |
964 | + "ldap_auth": "true", |
965 | + "ldap_password": "crapper", |
966 | + "ldap_user": "cn=admin,dc=test,dc=com", |
967 | + "ldap_base_dn": "dc=test,dc=com", |
968 | + "ldap_server": "ldap://{}".format(ldap_server_ip), |
969 | + "ldap_config_flags": ldap_config_flags, |
970 | + }, |
971 | + ) |
972 | + model.block_until_unit_wl_status(self.lead_unit_name, "active") |
973 | + model.block_until_all_units_idle() |
974 | + ldap_cfg = model.file_contents(self.lead_unit_name, config_file) |
975 | + self.assertIn("port = 389", ldap_cfg) |
976 | + self.assertIn('host = "{}"'.format(ldap_server_ip), ldap_cfg) |
977 | + # Check that server.group_mappings is not in the file. |
978 | + self.assertNotIn(expected_group_mappings, ldap_cfg) |
979 | + jane_req = self.query_grafana_website("janedoe", "crapper", "api/org/") |
980 | + self.assertEqual(jane_req.status_code, 200) |
981 | + self.assertIn("name", jane_req.json()) |
982 | + |
983 | + def test_23_configure_ldap_group_mappings(self): |
984 | + """Configure ldap group mappings and validate the correct user can login.""" |
985 | + config_file = LDAP_TOML[self.install_method] |
986 | + ldap_server_ip = model.get_app_ips("ldap-server")[0] |
987 | + model.set_application_config(self.application_name, {}) |
988 | + expected_group_mappings_admin = ( |
989 | + "[[servers.group_mappings]]\n" |
990 | + 'group_dn = "cn=admin,ou=groups,dc=test,dc=com"\n' |
991 | + 'org_role = "Admin"\n' |
992 | + "grafana_admin = true\n" |
993 | + ) |
994 | + expected_group_mappings_viewer = ( |
995 | + "[[servers.group_mappings]]\n" |
996 | + 'group_dn = "cn=openstack,ou=groups,dc=test,dc=com"\n' |
997 | + 'org_role = "Viewer"\n' |
998 | + ) |
999 | + ldap_config_flags = ( |
1000 | + "{" |
1001 | + "group_mappings_admin: 'cn=admin,ou=groups,dc=test,dc=com'," |
1002 | + "group_mappings_viewer: 'cn=openstack,ou=groups,dc=test,dc=com'," |
1003 | + "group_search_base_dns: 'ou=groups,dc=test,dc=com'," |
1004 | + "group_search_filter_user_attribute: 'uid'," |
1005 | + "group_search_filter: '(&(objectClass=posixGroup)(memberUid=%s))'," |
1006 | + "attribute_member_of: memberOf," |
1007 | + "attribute_email: email," |
1008 | + "attribute_name: givenname," |
1009 | + "attribute_surname: sn," |
1010 | + "attribute_username: cn," |
1011 | + "}" |
1012 | + ) |
1013 | + model.set_application_config( |
1014 | + self.application_name, |
1015 | + { |
1016 | + "ldap_auth": "true", |
1017 | + "ldap_password": "crapper", |
1018 | + "ldap_user": "cn=admin,dc=test,dc=com", |
1019 | + "ldap_base_dn": "dc=test,dc=com", |
1020 | + "ldap_server": "ldap://{}".format(ldap_server_ip), |
1021 | + "ldap_config_flags": ldap_config_flags, |
1022 | + }, |
1023 | + ) |
1024 | + model.block_until_unit_wl_status(self.lead_unit_name, "active") |
1025 | + model.block_until_all_units_idle() |
1026 | + ldap_cfg = model.file_contents(self.lead_unit_name, config_file) |
1027 | + self.assertIn(expected_group_mappings_admin, ldap_cfg) |
1028 | + self.assertIn(expected_group_mappings_viewer, ldap_cfg) |
1029 | + jane_req = self.query_grafana_website("janedoe", "crapper", "api/org/") |
1030 | + john_req = self.query_grafana_website("johndoe", "crapper", "api/org/") |
1031 | + self.assertEqual(jane_req.status_code, 401) |
1032 | + self.assertEqual(john_req.status_code, 200) |
1033 | + self.assertIn("name", john_req.json()) |
1034 | + |
1035 | + def test_24_remove_ldap_authentication(self): |
1036 | + """Disable LDAP authentication and validate that LDAP users can't login.""" |
1037 | + config_file = LDAP_TOML[self.install_method] |
1038 | + model.set_application_config(self.application_name, {"ldap_auth": "false"}) |
1039 | + model.block_until_unit_wl_status(self.lead_unit_name, "active") |
1040 | + model.block_until_all_units_idle() |
1041 | + expected_output = "cat: {}: No such file or directory".format(config_file) |
1042 | + ldap_cfg = model.run_on_unit( |
1043 | + self.lead_unit_name, |
1044 | + "cat %s" % config_file, |
1045 | + ) |
1046 | + self.assertIn(expected_output, ldap_cfg["Stderr"]) |
1047 | + john_req = self.query_grafana_website("johndoe", "crapper", "api/org/") |
1048 | + self.assertEqual(john_req.status_code, 401) |
1049 | + |
1050 | |
1051 | class SnappedGrafanaTest(BaseGrafanaTest): |
1052 | """Verify Grafana installed as a snap.""" |
1053 | @@ -365,6 +560,8 @@ class SnappedGrafanaTest(BaseGrafanaTest): |
1054 | status_message = self.get_unit_status(self.lead_unit_name) |
1055 | self.assertIn("PACKAGE UPGRADE REQUIRES MANUAL INTERVENTION", status_message) |
1056 | action = model.run_action(self.lead_unit_name, "do-upgrade") |
1057 | + model.block_until_unit_wl_status(self.lead_unit_name, "active") |
1058 | + model.block_until_all_units_idle() |
1059 | self.assertEqual(action.data["status"], "completed") |
1060 | self.assertIn("Upgraded", action.data["results"]["result"]) |
1061 | |
1062 | diff --git a/src/tests/unit/requirements.txt b/src/tests/unit/requirements.txt |
1063 | index cb975bd..42dfef1 100644 |
1064 | --- a/src/tests/unit/requirements.txt |
1065 | +++ b/src/tests/unit/requirements.txt |
1066 | @@ -7,3 +7,4 @@ requests |
1067 | jsondiff |
1068 | pbkdf2 |
1069 | pycrypto |
1070 | +netifaces |
A CI job is currently in progress. A follow up comment will be added when it completes.