Merge ~peter-sabaini/charm-grafana:bug/1875691-dashboard-upgrade into charm-grafana:master

Proposed by Peter Sabaini
Status: Merged
Approved by: Alvaro Uria
Approved revision: af7cea436183383c6a7af66a5c352f5e5ed8f1d8
Merged at revision: 963e29018ea7ec13e17b1bc69b6f8d697a8f4ed0
Proposed branch: ~peter-sabaini/charm-grafana:bug/1875691-dashboard-upgrade
Merge into: charm-grafana:master
Diff against target: 256 lines (+131/-34)
6 files modified
reactive/grafana.py (+40/-15)
tests/functional/tests/bundles/base.yaml (+9/-0)
tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 (+2/-0)
tests/functional/tests/bundles/overlays/focal.yaml.j2 (+2/-0)
tests/functional/tests/test_grafana.py (+76/-19)
tests/functional/tests/tests.yaml (+2/-0)
Reviewer Review Type Date Requested Status
Alvaro Uria (community) Approve
Giuseppe Petralia Approve
Zachary Zehring (community) Needs Fixing
BootStack Reviewers Pending
Review via email: mp+386564@code.launchpad.net

Commit message

Dashboard upgrade fix

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Peter Sabaini (peter-sabaini) wrote :
Revision history for this message
James Troup (elmo) wrote :

Some drive by commentary.

Revision history for this message
Zachary Zehring (zzehring) wrote :

Added comments inline.

review: Needs Fixing
Revision history for this message
Peter Sabaini (peter-sabaini) wrote :
Download full text (10.6 KiB)

Thanks for the review, I've added some comments and will push some changes shortly

On 01.07.20 17:52, Zachary Zehring wrote:
> Review: Needs Fixing
>
> Added comments inline.
>
> Diff comments:
>
>> diff --git a/reactive/grafana.py b/reactive/grafana.py
>> index c64743a..1b78d77 100644
>> --- a/reactive/grafana.py
>> +++ b/reactive/grafana.py
>> @@ -1006,22 +1006,59 @@ def hpwgen(passwd, salt):
>> return hpasswd
>>
>>
>> -@when("grafana.started", "endpoint.dashboards.requests")
>> +@when("grafana.started", "endpoint.dashboards.has_requests")
>> @when_not("grafana.change-block")
>> def import_dashboards(dashboards):
>> - for request in dashboards.new_requests:
>> + kv = unitdata.kv()
>> + dashboard_digests = kv.get("dash_digests", {})
>> +
>> + def dash_import(name, dash):
>
> Is there any reason that this is a nested function? I see it's really only to avoid passing in dashboard_digests. I'd prefer to move this out into it's own private function, as it's clunky to read here.

Reasoning behind making it nested is to hide it from outside use, as it's not meant to be used directly.

>> try:
>> - dash = request.dashboard
>> - if isinstance(request.dashboard, str):
>> - dash = json.loads(request.dashboard)
>> - success, reason = import_dashboard(dash, request.name)
>> + if isinstance(dash, str):
>> + # Safety: per the dashboard interface the dash should already
>
> I personally really dislike comments that describe intention that could be communicated through code. This check and possible assignment could be abstracted into a private function like 'ensure_dash_is_dict' which would be readable, less code in top function, and you could remove this block comment. Comments are often not updated when code changes, leading to misleading documentation. You could put any pertinent documentation into the docstring of the resulting function, which should be true describing the intention of the function.

I'm ok with abstracting this into a sep. function, but would hate to lose the comment as this is meant to explain the reason why this is being done.

>> + # be de-serialized json, but older dashboard providers used
>> + # to send a json string, so attempt to deserialize if we find
>> + # a string
>> + dash = json.loads(dash)
>> + success, reason = import_dashboard(dash, name)
>> + dashboard_digests[name] = digest
>> except json.decoder.JSONDecodeError as error:
>> success, reason = False, str(error)
>> -
>> hookenv.log(
>> - "import_dashboards via grafana-dashboards: {} [{}]".format(
>> - success, reason
>> - ),
>> - hookenv.DEBUG,
>> + "import_dashboards {}, digest {}: {} [{}]".format(
>> + name, digest, success, reason
>
> Since success is a boolean, this log message would be a bit awkward (like what exactly does true/false mean here). I'd adjust somewhat to be more clear in what just happened.

Fair

>> + )
>> )
>> + return succes...

Revision history for this message
Alvaro Uria (aluria) wrote :

I've added a few comments inline. Thank you.

review: Needs Fixing
Revision history for this message
Peter Sabaini (peter-sabaini) wrote :

Pushed some updates, one comment inline

Revision history for this message
Giuseppe Petralia (peppepetra) wrote :

All tests pass. Code looks good.

Just one minor comment inline on debug log, otherwise approved

review: Approve
Revision history for this message
Alvaro Uria (aluria) wrote :

Change lgtm, thank you.

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Failed to merge change (unable to merge source repository due to conflicts), setting status to needs review.

Revision history for this message
Alvaro Uria (aluria) wrote :

It seems it needs to be rebased

review: Needs Fixing
Revision history for this message
Peter Sabaini (peter-sabaini) wrote :

Rebase done. I've rerun functional and unit tests (note with the prometheus-libvirt-exporter from https://code.launchpad.net/~peter-sabaini/charm-prometheus-libvirt-exporter/+git/charm-prometheus-libvirt-exporter/+merge/386434), tests are passing

Revision history for this message
Alvaro Uria (aluria) wrote :

I've also run all tests and reviewed code one more time, and it lgtm. Cheers!

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 963e29018ea7ec13e17b1bc69b6f8d697a8f4ed0

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/reactive/grafana.py b/reactive/grafana.py
2index 8c39a9d..17dfad1 100644
3--- a/reactive/grafana.py
4+++ b/reactive/grafana.py
5@@ -1121,22 +1121,47 @@ def hpwgen(passwd, salt):
6 return hpasswd
7
8
9-@when("grafana.started", "endpoint.dashboards.requests")
10+def ensure_dash_deserialized(dash):
11+ """Deserialize dash.
12+
13+ Per the dashboard interface the dashboards transmitted over a relation
14+ should already be de-serialized json, but older dashboard providers used
15+ to send a json string, so attempt to deserialize if we find a string
16+ """
17+ if isinstance(dash, str):
18+ dash = json.loads(dash)
19+ return dash
20+
21+
22+@when("grafana.started", "endpoint.dashboards.has_requests")
23 @when_not("grafana.change-block")
24 def import_dashboards(dashboards):
25- for request in dashboards.new_requests:
26- try:
27- dash = request.dashboard
28- if isinstance(request.dashboard, str):
29- dash = json.loads(request.dashboard)
30- success, reason = import_dashboard(dash, request.name)
31- except json.decoder.JSONDecodeError as error:
32- success, reason = False, str(error)
33-
34+ kv = unitdata.kv()
35+ dashboard_digests = kv.get("dash_digests", {})
36+ new_requests = set([r.request_id for r in dashboards.new_requests])
37+ for req in dashboards.all_requests:
38+ dash = ensure_dash_deserialized(req.dashboard)
39+ is_new = req.request_id in new_requests
40+ digest = dash.get("digest")
41+ name = req.name
42 hookenv.log(
43- "import_dashboards via grafana-dashboards: {} [{}]".format(
44- success, reason
45- ),
46- hookenv.DEBUG,
47+ "import_dashboards: {}, digest {}, is_new: {}".format(
48+ name, digest, is_new
49+ )
50 )
51- request.respond(success, reason)
52+ if not digest and not is_new:
53+ # Backward compat: if we don't see a digest in the payload, it's
54+ # coming from an older provider. Skip unless it's new
55+ continue
56+ if dashboard_digests.get(name) != digest or is_new:
57+ success, reason = import_dashboard(dash, name)
58+ dashboard_digests[name] = digest
59+ hookenv.log(
60+ "import_dashboards {}, digest {}: success: {} [{}]".format(
61+ name, digest, success, reason
62+ )
63+ )
64+ if is_new:
65+ req.respond(success, reason)
66+
67+ kv.set("dash_digests", dashboard_digests)
68diff --git a/tests/functional/tests/bundles/base.yaml b/tests/functional/tests/bundles/base.yaml
69index 7b8d346..7994657 100644
70--- a/tests/functional/tests/bundles/base.yaml
71+++ b/tests/functional/tests/bundles/base.yaml
72@@ -49,6 +49,9 @@ applications:
73 prometheus-ceph-exporter:
74 charm: cs:prometheus-ceph-exporter
75 num_units: 1
76+ prometheus-libvirt-exporter:
77+ charm: cs:prometheus-libvirt-exporter
78+
79
80 relations:
81 - - keystone:shared-db
82@@ -75,3 +78,9 @@ relations:
83 - grafana:grafana-source
84 - - grafana:nrpe-external-master
85 - nrpe
86+- - prometheus-libvirt-exporter
87+ - mysql
88+- - prometheus:target
89+ - prometheus-libvirt-exporter
90+- - grafana:dashboards
91+ - prometheus-libvirt-exporter
92diff --git a/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2 b/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2
93index 49b191a..446a332 100644
94--- a/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2
95+++ b/tests/functional/tests/bundles/overlays/focal-snap.yaml.j2
96@@ -21,3 +21,5 @@ applications:
97 series: bionic
98 prometheus-ceph-exporter:
99 series: bionic
100+ prometheus-libvirt-exporter:
101+ series: bionic
102diff --git a/tests/functional/tests/bundles/overlays/focal.yaml.j2 b/tests/functional/tests/bundles/overlays/focal.yaml.j2
103index 452a798..5734e6c 100644
104--- a/tests/functional/tests/bundles/overlays/focal.yaml.j2
105+++ b/tests/functional/tests/bundles/overlays/focal.yaml.j2
106@@ -20,3 +20,5 @@ applications:
107 series: bionic
108 prometheus-ceph-exporter:
109 series: bionic
110+ prometheus-libvirt-exporter:
111+ series: bionic
112diff --git a/tests/functional/tests/test_grafana.py b/tests/functional/tests/test_grafana.py
113index 5f452d9..31d3b4d 100644
114--- a/tests/functional/tests/test_grafana.py
115+++ b/tests/functional/tests/test_grafana.py
116@@ -16,6 +16,8 @@ DEFAULT_API_URL = "/"
117 class BaseGrafanaTest(unittest.TestCase):
118 """Base for Prometheus-openstack-exporter charm tests."""
119
120+ _admin_pass = None
121+
122 @classmethod
123 def setUpClass(cls):
124 """Set up tests."""
125@@ -34,6 +36,43 @@ class BaseGrafanaTest(unittest.TestCase):
126 u = model.get_unit_from_name(unit)
127 return u.workload_status_message
128
129+ @property
130+ def admin_password(self):
131+ if self._admin_pass is None:
132+ action = model.run_action(
133+ self.lead_unit_name,
134+ "get-admin-password",
135+ raise_on_failure=True,
136+ )
137+ self._admin_pass = action.data["results"]["password"]
138+ return self._admin_pass
139+
140+ def get_and_check_dashboards(self, cond):
141+ """Retrieve dashboard from grafana
142+
143+ Check if cond(dashboards) is true, otherwise retry up to TEST_TIMEOUT
144+ """
145+ timeout = time.time() + TEST_TIMEOUT
146+ dashboards = []
147+ while True:
148+ req = requests.get(
149+ "http://{host}:{port}"
150+ "/api/search?dashboardIds".format(
151+ host=self.grafana_ip, port=DEFAULT_API_PORT
152+ ),
153+ auth=("admin", self.admin_password),
154+ )
155+ self.assertTrue(req.status_code == 200)
156+ dashboards = json.loads(req.text)
157+ if time.time() > timeout or cond(dashboards):
158+ break
159+ time.sleep(30)
160+ return dashboards
161+
162+ def remote_sed(self, unit, file, expr):
163+ cmd = ["sed", "-i", "-e", expr, file]
164+ model.run_on_unit(unit, " ".join(cmd))
165+
166
167 class CharmOperationTest(BaseGrafanaTest):
168 """Verify operations."""
169@@ -99,25 +138,9 @@ class CharmOperationTest(BaseGrafanaTest):
170
171 def test_10_grafana_imported_dashboards(self):
172 """Check that Grafana dashboards expected are there"""
173- action = model.run_action(
174- self.lead_unit_name, "get-admin-password", raise_on_failure=True,
175+ dashboards = self.get_and_check_dashboards(
176+ lambda dashes: len(dashes) >= 4
177 )
178- passwd = action.data["results"]["password"]
179- # Loop this, it takes a while for the data to arrive and dashboards
180- # to populate
181- timeout = time.time() + TEST_TIMEOUT
182- dashboards = []
183- while time.time() < timeout and len(dashboards) < 4:
184- req = requests.get(
185- "http://{host}:{port}"
186- "/api/search?dashboardIds".format(
187- host=self.grafana_ip, port=DEFAULT_API_PORT
188- ),
189- auth=("admin", passwd),
190- )
191- self.assertTrue(req.status_code == 200)
192- dashboards = json.loads(req.text)
193- time.sleep(30)
194 self.assertTrue(len(dashboards) >= 4)
195 self.assertTrue(
196 all(
197@@ -127,6 +150,40 @@ class CharmOperationTest(BaseGrafanaTest):
198 )
199 )
200
201+ def test_11_dashboard_upgrade(self):
202+ """Test upgraded dashboards are imported."""
203+ exporter = "prometheus-libvirt-exporter/0"
204+ self.remote_sed(
205+ exporter,
206+ "/var/lib/juju/agents/unit-prometheus-libvirt-exporter-0/charm/"
207+ "files/grafana-dashboards/libvirt.json",
208+ "s/Libvirt/FunctionalTestMarker/",
209+ )
210+ model.run_on_unit(
211+ exporter, "JUJU_HOOK_NAME=upgrade-charm ./hooks/upgrade-charm"
212+ )
213+ model.block_until_all_units_idle()
214+
215+ def chk(dashes):
216+ marker_dashes = [
217+ d for d in dashes if "FunctionalTestMarker" in d["title"]
218+ ]
219+ return len(dashes) >= 4 and len(marker_dashes) == 1
220+
221+ dashes = self.get_and_check_dashboards(chk)
222+ self.assertTrue(chk(dashes))
223+
224+ self.remote_sed(
225+ exporter,
226+ "/var/lib/juju/agents/unit-prometheus-libvirt-exporter-0/charm/"
227+ "files/grafana-dashboards/libvirt.json",
228+ "s/FunctionalTestMarker/Libvirt/",
229+ )
230+ model.run_on_unit(
231+ exporter, "JUJU_HOOK_NAME=upgrade-charm ./hooks/upgrade-charm"
232+ )
233+ model.block_until_all_units_idle()
234+
235 def test_12_grafana_create_user(self):
236 """Run the create-user action."""
237 params = {
238@@ -153,7 +210,7 @@ class CharmOperationTest(BaseGrafanaTest):
239
240 class SnappedGrafanaTest(BaseGrafanaTest):
241 def test_50_snap_upgrade(self):
242- """Test a snap upgrade from 6 to 7"""
243+ """Test a snap upgrade from 6 to 7."""
244 model.set_application_config(
245 self.application_name, {"snap_channel": "7/stable"}
246 )
247diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml
248index 56abe83..909b776 100644
249--- a/tests/functional/tests/tests.yaml
250+++ b/tests/functional/tests/tests.yaml
251@@ -23,3 +23,5 @@ target_deploy_status:
252 workload-status-message: Running
253 telegraf:
254 workload-status-message: Monitoring
255+ prometheus-libvirt-exporter:
256+ workload-status-message: "Exporter installed and connected to libvirt slot"

Subscribers

People subscribed via source and target branches

to all changes: