Merge lp:~brad-marshall/charms/trusty/percona-cluster/fix-nagios into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next
- Trusty Tahr (14.04)
- fix-nagios
- Merge into next
Proposed by
Brad Marshall
Status: | Merged |
---|---|
Merged at revision: | 56 |
Proposed branch: | lp:~brad-marshall/charms/trusty/percona-cluster/fix-nagios |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/percona-cluster/next |
Diff against target: |
1715 lines (+584/-522) 9 files modified
charm-helpers.yaml (+1/-0) config.yaml (+16/-1) hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0) hooks/charmhelpers/contrib/charmsupport/nrpe.py (+358/-0) hooks/charmhelpers/contrib/charmsupport/volumes.py (+175/-0) hooks/charmhelpers/contrib/database/mysql.py (+0/-2) hooks/charmhelpers/core/strutils.py (+0/-42) hooks/charmhelpers/core/unitdata.py (+0/-477) hooks/percona_hooks.py (+19/-0) |
To merge this branch: | bzr merge lp:~brad-marshall/charms/trusty/percona-cluster/fix-nagios |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
Brad Marshall (community) | Needs Resubmitting | ||
Review via email: mp+250702@code.launchpad.net |
Commit message
Description of the change
Synced charmhelpers, added nagios_servicegroup config option
To post a comment you must log in.
- 48. By Liam Young
-
[hopem, r=niedbalski,gnuoy] Synced charm-helpers to get fix for LP #1423153
- 49. By Billy Olsen
-
[hopem, r=wolsen] sync charm-helpers fix for LP #1425999
- 50. By Edward Hope-Morley
-
Reverted commit r49
- 51. By Jorge Niedbalski
-
[hopem, r=billy-olsen, niedbalski] Fixes bug LP: #1425999
- 52. By Brad Marshall
-
[bradm] Add nagios checks, sync charmhelpers, merged with upstream
Revision history for this message
Brad Marshall (brad-marshall) wrote : | # |
This should be fixed, and now ready for a re-review
review:
Needs Resubmitting
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charm-helpers.yaml' | |||
2 | --- charm-helpers.yaml 2015-02-10 11:16:27 +0000 | |||
3 | +++ charm-helpers.yaml 2015-03-03 02:26:44 +0000 | |||
4 | @@ -8,3 +8,4 @@ | |||
5 | 8 | - payload.execd | 8 | - payload.execd |
6 | 9 | - contrib.network.ip | 9 | - contrib.network.ip |
7 | 10 | - contrib.database | 10 | - contrib.database |
8 | 11 | - contrib.charmsupport | ||
9 | 11 | 12 | ||
10 | === modified file 'config.yaml' | |||
11 | --- config.yaml 2015-02-16 14:12:42 +0000 | |||
12 | +++ config.yaml 2015-03-03 02:26:44 +0000 | |||
13 | @@ -77,4 +77,19 @@ | |||
14 | 77 | type: boolean | 77 | type: boolean |
15 | 78 | default: False | 78 | default: False |
16 | 79 | description: Adds two config options (wsrep_drupal_282555_workaround and wsrep_retry_autocommit) as a workaround for Percona Primary Key bug (see lplp1366997). | 79 | description: Adds two config options (wsrep_drupal_282555_workaround and wsrep_retry_autocommit) as a workaround for Percona Primary Key bug (see lplp1366997). |
18 | 80 | 80 | nagios_context: | |
19 | 81 | default: "juju" | ||
20 | 82 | type: string | ||
21 | 83 | description: | | ||
22 | 84 | Used by the nrpe-external-master subordinate charm. | ||
23 | 85 | A string that will be prepended to instance name to set the host name | ||
24 | 86 | in nagios. So for instance the hostname would be something like: | ||
25 | 87 | juju-myservice-0 | ||
26 | 88 | If you're running multiple environments with the same services in them | ||
27 | 89 | this allows you to differentiate between them. | ||
28 | 90 | nagios_servicegroups: | ||
29 | 91 | default: "" | ||
30 | 92 | type: string | ||
31 | 93 | description: | | ||
32 | 94 | A comma-separated list of nagios servicegroups. | ||
33 | 95 | If left empty, the nagios_context will be used as the servicegroup | ||
34 | 81 | 96 | ||
35 | === added directory 'hooks/charmhelpers/contrib/charmsupport' | |||
36 | === added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py' | |||
37 | --- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000 | |||
38 | +++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-03-03 02:26:44 +0000 | |||
39 | @@ -0,0 +1,15 @@ | |||
40 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
41 | 2 | # | ||
42 | 3 | # This file is part of charm-helpers. | ||
43 | 4 | # | ||
44 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
45 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
46 | 7 | # published by the Free Software Foundation. | ||
47 | 8 | # | ||
48 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
49 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
50 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
51 | 12 | # GNU Lesser General Public License for more details. | ||
52 | 13 | # | ||
53 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
54 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
55 | 0 | 16 | ||
56 | === added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py' | |||
57 | --- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 | |||
58 | +++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-03 02:26:44 +0000 | |||
59 | @@ -0,0 +1,358 @@ | |||
60 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
61 | 2 | # | ||
62 | 3 | # This file is part of charm-helpers. | ||
63 | 4 | # | ||
64 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
65 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
66 | 7 | # published by the Free Software Foundation. | ||
67 | 8 | # | ||
68 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
69 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
70 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
71 | 12 | # GNU Lesser General Public License for more details. | ||
72 | 13 | # | ||
73 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
74 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
75 | 16 | |||
76 | 17 | """Compatibility with the nrpe-external-master charm""" | ||
77 | 18 | # Copyright 2012 Canonical Ltd. | ||
78 | 19 | # | ||
79 | 20 | # Authors: | ||
80 | 21 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
81 | 22 | |||
82 | 23 | import subprocess | ||
83 | 24 | import pwd | ||
84 | 25 | import grp | ||
85 | 26 | import os | ||
86 | 27 | import glob | ||
87 | 28 | import shutil | ||
88 | 29 | import re | ||
89 | 30 | import shlex | ||
90 | 31 | import yaml | ||
91 | 32 | |||
92 | 33 | from charmhelpers.core.hookenv import ( | ||
93 | 34 | config, | ||
94 | 35 | local_unit, | ||
95 | 36 | log, | ||
96 | 37 | relation_ids, | ||
97 | 38 | relation_set, | ||
98 | 39 | relations_of_type, | ||
99 | 40 | ) | ||
100 | 41 | |||
101 | 42 | from charmhelpers.core.host import service | ||
102 | 43 | |||
103 | 44 | # This module adds compatibility with the nrpe-external-master and plain nrpe | ||
104 | 45 | # subordinate charms. To use it in your charm: | ||
105 | 46 | # | ||
106 | 47 | # 1. Update metadata.yaml | ||
107 | 48 | # | ||
108 | 49 | # provides: | ||
109 | 50 | # (...) | ||
110 | 51 | # nrpe-external-master: | ||
111 | 52 | # interface: nrpe-external-master | ||
112 | 53 | # scope: container | ||
113 | 54 | # | ||
114 | 55 | # and/or | ||
115 | 56 | # | ||
116 | 57 | # provides: | ||
117 | 58 | # (...) | ||
118 | 59 | # local-monitors: | ||
119 | 60 | # interface: local-monitors | ||
120 | 61 | # scope: container | ||
121 | 62 | |||
122 | 63 | # | ||
123 | 64 | # 2. Add the following to config.yaml | ||
124 | 65 | # | ||
125 | 66 | # nagios_context: | ||
126 | 67 | # default: "juju" | ||
127 | 68 | # type: string | ||
128 | 69 | # description: | | ||
129 | 70 | # Used by the nrpe subordinate charms. | ||
130 | 71 | # A string that will be prepended to instance name to set the host name | ||
131 | 72 | # in nagios. So for instance the hostname would be something like: | ||
132 | 73 | # juju-myservice-0 | ||
133 | 74 | # If you're running multiple environments with the same services in them | ||
134 | 75 | # this allows you to differentiate between them. | ||
135 | 76 | # nagios_servicegroups: | ||
136 | 77 | # default: "" | ||
137 | 78 | # type: string | ||
138 | 79 | # description: | | ||
139 | 80 | # A comma-separated list of nagios servicegroups. | ||
140 | 81 | # If left empty, the nagios_context will be used as the servicegroup | ||
141 | 82 | # | ||
142 | 83 | # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master | ||
143 | 84 | # | ||
144 | 85 | # 4. Update your hooks.py with something like this: | ||
145 | 86 | # | ||
146 | 87 | # from charmsupport.nrpe import NRPE | ||
147 | 88 | # (...) | ||
148 | 89 | # def update_nrpe_config(): | ||
149 | 90 | # nrpe_compat = NRPE() | ||
150 | 91 | # nrpe_compat.add_check( | ||
151 | 92 | # shortname = "myservice", | ||
152 | 93 | # description = "Check MyService", | ||
153 | 94 | # check_cmd = "check_http -w 2 -c 10 http://localhost" | ||
154 | 95 | # ) | ||
155 | 96 | # nrpe_compat.add_check( | ||
156 | 97 | # "myservice_other", | ||
157 | 98 | # "Check for widget failures", | ||
158 | 99 | # check_cmd = "/srv/myapp/scripts/widget_check" | ||
159 | 100 | # ) | ||
160 | 101 | # nrpe_compat.write() | ||
161 | 102 | # | ||
162 | 103 | # def config_changed(): | ||
163 | 104 | # (...) | ||
164 | 105 | # update_nrpe_config() | ||
165 | 106 | # | ||
166 | 107 | # def nrpe_external_master_relation_changed(): | ||
167 | 108 | # update_nrpe_config() | ||
168 | 109 | # | ||
169 | 110 | # def local_monitors_relation_changed(): | ||
170 | 111 | # update_nrpe_config() | ||
171 | 112 | # | ||
172 | 113 | # 5. ln -s hooks.py nrpe-external-master-relation-changed | ||
173 | 114 | # ln -s hooks.py local-monitors-relation-changed | ||
174 | 115 | |||
175 | 116 | |||
176 | 117 | class CheckException(Exception): | ||
177 | 118 | pass | ||
178 | 119 | |||
179 | 120 | |||
180 | 121 | class Check(object): | ||
181 | 122 | shortname_re = '[A-Za-z0-9-_]+$' | ||
182 | 123 | service_template = (""" | ||
183 | 124 | #--------------------------------------------------- | ||
184 | 125 | # This file is Juju managed | ||
185 | 126 | #--------------------------------------------------- | ||
186 | 127 | define service {{ | ||
187 | 128 | use active-service | ||
188 | 129 | host_name {nagios_hostname} | ||
189 | 130 | service_description {nagios_hostname}[{shortname}] """ | ||
190 | 131 | """{description} | ||
191 | 132 | check_command check_nrpe!{command} | ||
192 | 133 | servicegroups {nagios_servicegroup} | ||
193 | 134 | }} | ||
194 | 135 | """) | ||
195 | 136 | |||
196 | 137 | def __init__(self, shortname, description, check_cmd): | ||
197 | 138 | super(Check, self).__init__() | ||
198 | 139 | # XXX: could be better to calculate this from the service name | ||
199 | 140 | if not re.match(self.shortname_re, shortname): | ||
200 | 141 | raise CheckException("shortname must match {}".format( | ||
201 | 142 | Check.shortname_re)) | ||
202 | 143 | self.shortname = shortname | ||
203 | 144 | self.command = "check_{}".format(shortname) | ||
204 | 145 | # Note: a set of invalid characters is defined by the | ||
205 | 146 | # Nagios server config | ||
206 | 147 | # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= | ||
207 | 148 | self.description = description | ||
208 | 149 | self.check_cmd = self._locate_cmd(check_cmd) | ||
209 | 150 | |||
210 | 151 | def _locate_cmd(self, check_cmd): | ||
211 | 152 | search_path = ( | ||
212 | 153 | '/usr/lib/nagios/plugins', | ||
213 | 154 | '/usr/local/lib/nagios/plugins', | ||
214 | 155 | ) | ||
215 | 156 | parts = shlex.split(check_cmd) | ||
216 | 157 | for path in search_path: | ||
217 | 158 | if os.path.exists(os.path.join(path, parts[0])): | ||
218 | 159 | command = os.path.join(path, parts[0]) | ||
219 | 160 | if len(parts) > 1: | ||
220 | 161 | command += " " + " ".join(parts[1:]) | ||
221 | 162 | return command | ||
222 | 163 | log('Check command not found: {}'.format(parts[0])) | ||
223 | 164 | return '' | ||
224 | 165 | |||
225 | 166 | def write(self, nagios_context, hostname, nagios_servicegroups): | ||
226 | 167 | nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format( | ||
227 | 168 | self.command) | ||
228 | 169 | with open(nrpe_check_file, 'w') as nrpe_check_config: | ||
229 | 170 | nrpe_check_config.write("# check {}\n".format(self.shortname)) | ||
230 | 171 | nrpe_check_config.write("command[{}]={}\n".format( | ||
231 | 172 | self.command, self.check_cmd)) | ||
232 | 173 | |||
233 | 174 | if not os.path.exists(NRPE.nagios_exportdir): | ||
234 | 175 | log('Not writing service config as {} is not accessible'.format( | ||
235 | 176 | NRPE.nagios_exportdir)) | ||
236 | 177 | else: | ||
237 | 178 | self.write_service_config(nagios_context, hostname, | ||
238 | 179 | nagios_servicegroups) | ||
239 | 180 | |||
240 | 181 | def write_service_config(self, nagios_context, hostname, | ||
241 | 182 | nagios_servicegroups): | ||
242 | 183 | for f in os.listdir(NRPE.nagios_exportdir): | ||
243 | 184 | if re.search('.*{}.cfg'.format(self.command), f): | ||
244 | 185 | os.remove(os.path.join(NRPE.nagios_exportdir, f)) | ||
245 | 186 | |||
246 | 187 | templ_vars = { | ||
247 | 188 | 'nagios_hostname': hostname, | ||
248 | 189 | 'nagios_servicegroup': nagios_servicegroups, | ||
249 | 190 | 'description': self.description, | ||
250 | 191 | 'shortname': self.shortname, | ||
251 | 192 | 'command': self.command, | ||
252 | 193 | } | ||
253 | 194 | nrpe_service_text = Check.service_template.format(**templ_vars) | ||
254 | 195 | nrpe_service_file = '{}/service__{}_{}.cfg'.format( | ||
255 | 196 | NRPE.nagios_exportdir, hostname, self.command) | ||
256 | 197 | with open(nrpe_service_file, 'w') as nrpe_service_config: | ||
257 | 198 | nrpe_service_config.write(str(nrpe_service_text)) | ||
258 | 199 | |||
259 | 200 | def run(self): | ||
260 | 201 | subprocess.call(self.check_cmd) | ||
261 | 202 | |||
262 | 203 | |||
263 | 204 | class NRPE(object): | ||
264 | 205 | nagios_logdir = '/var/log/nagios' | ||
265 | 206 | nagios_exportdir = '/var/lib/nagios/export' | ||
266 | 207 | nrpe_confdir = '/etc/nagios/nrpe.d' | ||
267 | 208 | |||
268 | 209 | def __init__(self, hostname=None): | ||
269 | 210 | super(NRPE, self).__init__() | ||
270 | 211 | self.config = config() | ||
271 | 212 | self.nagios_context = self.config['nagios_context'] | ||
272 | 213 | if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: | ||
273 | 214 | self.nagios_servicegroups = self.config['nagios_servicegroups'] | ||
274 | 215 | else: | ||
275 | 216 | self.nagios_servicegroups = self.nagios_context | ||
276 | 217 | self.unit_name = local_unit().replace('/', '-') | ||
277 | 218 | if hostname: | ||
278 | 219 | self.hostname = hostname | ||
279 | 220 | else: | ||
280 | 221 | self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) | ||
281 | 222 | self.checks = [] | ||
282 | 223 | |||
283 | 224 | def add_check(self, *args, **kwargs): | ||
284 | 225 | self.checks.append(Check(*args, **kwargs)) | ||
285 | 226 | |||
286 | 227 | def write(self): | ||
287 | 228 | try: | ||
288 | 229 | nagios_uid = pwd.getpwnam('nagios').pw_uid | ||
289 | 230 | nagios_gid = grp.getgrnam('nagios').gr_gid | ||
290 | 231 | except: | ||
291 | 232 | log("Nagios user not set up, nrpe checks not updated") | ||
292 | 233 | return | ||
293 | 234 | |||
294 | 235 | if not os.path.exists(NRPE.nagios_logdir): | ||
295 | 236 | os.mkdir(NRPE.nagios_logdir) | ||
296 | 237 | os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) | ||
297 | 238 | |||
298 | 239 | nrpe_monitors = {} | ||
299 | 240 | monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} | ||
300 | 241 | for nrpecheck in self.checks: | ||
301 | 242 | nrpecheck.write(self.nagios_context, self.hostname, | ||
302 | 243 | self.nagios_servicegroups) | ||
303 | 244 | nrpe_monitors[nrpecheck.shortname] = { | ||
304 | 245 | "command": nrpecheck.command, | ||
305 | 246 | } | ||
306 | 247 | |||
307 | 248 | service('restart', 'nagios-nrpe-server') | ||
308 | 249 | |||
309 | 250 | for rid in relation_ids("local-monitors"): | ||
310 | 251 | relation_set(relation_id=rid, monitors=yaml.dump(monitors)) | ||
311 | 252 | |||
312 | 253 | |||
313 | 254 | def get_nagios_hostcontext(relation_name='nrpe-external-master'): | ||
314 | 255 | """ | ||
315 | 256 | Query relation with nrpe subordinate, return the nagios_host_context | ||
316 | 257 | |||
317 | 258 | :param str relation_name: Name of relation nrpe sub joined to | ||
318 | 259 | """ | ||
319 | 260 | for rel in relations_of_type(relation_name): | ||
320 | 261 | if 'nagios_hostname' in rel: | ||
321 | 262 | return rel['nagios_host_context'] | ||
322 | 263 | |||
323 | 264 | |||
324 | 265 | def get_nagios_hostname(relation_name='nrpe-external-master'): | ||
325 | 266 | """ | ||
326 | 267 | Query relation with nrpe subordinate, return the nagios_hostname | ||
327 | 268 | |||
328 | 269 | :param str relation_name: Name of relation nrpe sub joined to | ||
329 | 270 | """ | ||
330 | 271 | for rel in relations_of_type(relation_name): | ||
331 | 272 | if 'nagios_hostname' in rel: | ||
332 | 273 | return rel['nagios_hostname'] | ||
333 | 274 | |||
334 | 275 | |||
335 | 276 | def get_nagios_unit_name(relation_name='nrpe-external-master'): | ||
336 | 277 | """ | ||
337 | 278 | Return the nagios unit name prepended with host_context if needed | ||
338 | 279 | |||
339 | 280 | :param str relation_name: Name of relation nrpe sub joined to | ||
340 | 281 | """ | ||
341 | 282 | host_context = get_nagios_hostcontext(relation_name) | ||
342 | 283 | if host_context: | ||
343 | 284 | unit = "%s:%s" % (host_context, local_unit()) | ||
344 | 285 | else: | ||
345 | 286 | unit = local_unit() | ||
346 | 287 | return unit | ||
347 | 288 | |||
348 | 289 | |||
349 | 290 | def add_init_service_checks(nrpe, services, unit_name): | ||
350 | 291 | """ | ||
351 | 292 | Add checks for each service in list | ||
352 | 293 | |||
353 | 294 | :param NRPE nrpe: NRPE object to add check to | ||
354 | 295 | :param list services: List of services to check | ||
355 | 296 | :param str unit_name: Unit name to use in check description | ||
356 | 297 | """ | ||
357 | 298 | for svc in services: | ||
358 | 299 | upstart_init = '/etc/init/%s.conf' % svc | ||
359 | 300 | sysv_init = '/etc/init.d/%s' % svc | ||
360 | 301 | if os.path.exists(upstart_init): | ||
361 | 302 | nrpe.add_check( | ||
362 | 303 | shortname=svc, | ||
363 | 304 | description='process check {%s}' % unit_name, | ||
364 | 305 | check_cmd='check_upstart_job %s' % svc | ||
365 | 306 | ) | ||
366 | 307 | elif os.path.exists(sysv_init): | ||
367 | 308 | cronpath = '/etc/cron.d/nagios-service-check-%s' % svc | ||
368 | 309 | cron_file = ('*/5 * * * * root ' | ||
369 | 310 | '/usr/local/lib/nagios/plugins/check_exit_status.pl ' | ||
370 | 311 | '-s /etc/init.d/%s status > ' | ||
371 | 312 | '/var/lib/nagios/service-check-%s.txt\n' % (svc, | ||
372 | 313 | svc) | ||
373 | 314 | ) | ||
374 | 315 | f = open(cronpath, 'w') | ||
375 | 316 | f.write(cron_file) | ||
376 | 317 | f.close() | ||
377 | 318 | nrpe.add_check( | ||
378 | 319 | shortname=svc, | ||
379 | 320 | description='process check {%s}' % unit_name, | ||
380 | 321 | check_cmd='check_status_file.py -f ' | ||
381 | 322 | '/var/lib/nagios/service-check-%s.txt' % svc, | ||
382 | 323 | ) | ||
383 | 324 | |||
384 | 325 | |||
385 | 326 | def copy_nrpe_checks(): | ||
386 | 327 | """ | ||
387 | 328 | Copy the nrpe checks into place | ||
388 | 329 | |||
389 | 330 | """ | ||
390 | 331 | NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' | ||
391 | 332 | nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks', | ||
392 | 333 | 'charmhelpers', 'contrib', 'openstack', | ||
393 | 334 | 'files') | ||
394 | 335 | |||
395 | 336 | if not os.path.exists(NAGIOS_PLUGINS): | ||
396 | 337 | os.makedirs(NAGIOS_PLUGINS) | ||
397 | 338 | for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")): | ||
398 | 339 | if os.path.isfile(fname): | ||
399 | 340 | shutil.copy2(fname, | ||
400 | 341 | os.path.join(NAGIOS_PLUGINS, os.path.basename(fname))) | ||
401 | 342 | |||
402 | 343 | |||
403 | 344 | def add_haproxy_checks(nrpe, unit_name): | ||
404 | 345 | """ | ||
405 | 346 | Add checks for each service in list | ||
406 | 347 | |||
407 | 348 | :param NRPE nrpe: NRPE object to add check to | ||
408 | 349 | :param str unit_name: Unit name to use in check description | ||
409 | 350 | """ | ||
410 | 351 | nrpe.add_check( | ||
411 | 352 | shortname='haproxy_servers', | ||
412 | 353 | description='Check HAProxy {%s}' % unit_name, | ||
413 | 354 | check_cmd='check_haproxy.sh') | ||
414 | 355 | nrpe.add_check( | ||
415 | 356 | shortname='haproxy_queue', | ||
416 | 357 | description='Check HAProxy queue depth {%s}' % unit_name, | ||
417 | 358 | check_cmd='check_haproxy_queue_depth.sh') | ||
418 | 0 | 359 | ||
419 | === added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py' | |||
420 | --- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 | |||
421 | +++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-03-03 02:26:44 +0000 | |||
422 | @@ -0,0 +1,175 @@ | |||
423 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
424 | 2 | # | ||
425 | 3 | # This file is part of charm-helpers. | ||
426 | 4 | # | ||
427 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
428 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
429 | 7 | # published by the Free Software Foundation. | ||
430 | 8 | # | ||
431 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
432 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
433 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
434 | 12 | # GNU Lesser General Public License for more details. | ||
435 | 13 | # | ||
436 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
437 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
438 | 16 | |||
439 | 17 | ''' | ||
440 | 18 | Functions for managing volumes in juju units. One volume is supported per unit. | ||
441 | 19 | Subordinates may have their own storage, provided it is on its own partition. | ||
442 | 20 | |||
443 | 21 | Configuration stanzas:: | ||
444 | 22 | |||
445 | 23 | volume-ephemeral: | ||
446 | 24 | type: boolean | ||
447 | 25 | default: true | ||
448 | 26 | description: > | ||
449 | 27 | If false, a volume is mounted as sepecified in "volume-map" | ||
450 | 28 | If true, ephemeral storage will be used, meaning that log data | ||
451 | 29 | will only exist as long as the machine. YOU HAVE BEEN WARNED. | ||
452 | 30 | volume-map: | ||
453 | 31 | type: string | ||
454 | 32 | default: {} | ||
455 | 33 | description: > | ||
456 | 34 | YAML map of units to device names, e.g: | ||
457 | 35 | "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" | ||
458 | 36 | Service units will raise a configure-error if volume-ephemeral | ||
459 | 37 | is 'true' and no volume-map value is set. Use 'juju set' to set a | ||
460 | 38 | value and 'juju resolved' to complete configuration. | ||
461 | 39 | |||
462 | 40 | Usage:: | ||
463 | 41 | |||
464 | 42 | from charmsupport.volumes import configure_volume, VolumeConfigurationError | ||
465 | 43 | from charmsupport.hookenv import log, ERROR | ||
466 | 44 | def post_mount_hook(): | ||
467 | 45 | stop_service('myservice') | ||
468 | 46 | def post_mount_hook(): | ||
469 | 47 | start_service('myservice') | ||
470 | 48 | |||
471 | 49 | if __name__ == '__main__': | ||
472 | 50 | try: | ||
473 | 51 | configure_volume(before_change=pre_mount_hook, | ||
474 | 52 | after_change=post_mount_hook) | ||
475 | 53 | except VolumeConfigurationError: | ||
476 | 54 | log('Storage could not be configured', ERROR) | ||
477 | 55 | |||
478 | 56 | ''' | ||
479 | 57 | |||
480 | 58 | # XXX: Known limitations | ||
481 | 59 | # - fstab is neither consulted nor updated | ||
482 | 60 | |||
483 | 61 | import os | ||
484 | 62 | from charmhelpers.core import hookenv | ||
485 | 63 | from charmhelpers.core import host | ||
486 | 64 | import yaml | ||
487 | 65 | |||
488 | 66 | |||
489 | 67 | MOUNT_BASE = '/srv/juju/volumes' | ||
490 | 68 | |||
491 | 69 | |||
492 | 70 | class VolumeConfigurationError(Exception): | ||
493 | 71 | '''Volume configuration data is missing or invalid''' | ||
494 | 72 | pass | ||
495 | 73 | |||
496 | 74 | |||
497 | 75 | def get_config(): | ||
498 | 76 | '''Gather and sanity-check volume configuration data''' | ||
499 | 77 | volume_config = {} | ||
500 | 78 | config = hookenv.config() | ||
501 | 79 | |||
502 | 80 | errors = False | ||
503 | 81 | |||
504 | 82 | if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): | ||
505 | 83 | volume_config['ephemeral'] = True | ||
506 | 84 | else: | ||
507 | 85 | volume_config['ephemeral'] = False | ||
508 | 86 | |||
509 | 87 | try: | ||
510 | 88 | volume_map = yaml.safe_load(config.get('volume-map', '{}')) | ||
511 | 89 | except yaml.YAMLError as e: | ||
512 | 90 | hookenv.log("Error parsing YAML volume-map: {}".format(e), | ||
513 | 91 | hookenv.ERROR) | ||
514 | 92 | errors = True | ||
515 | 93 | if volume_map is None: | ||
516 | 94 | # probably an empty string | ||
517 | 95 | volume_map = {} | ||
518 | 96 | elif not isinstance(volume_map, dict): | ||
519 | 97 | hookenv.log("Volume-map should be a dictionary, not {}".format( | ||
520 | 98 | type(volume_map))) | ||
521 | 99 | errors = True | ||
522 | 100 | |||
523 | 101 | volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) | ||
524 | 102 | if volume_config['device'] and volume_config['ephemeral']: | ||
525 | 103 | # asked for ephemeral storage but also defined a volume ID | ||
526 | 104 | hookenv.log('A volume is defined for this unit, but ephemeral ' | ||
527 | 105 | 'storage was requested', hookenv.ERROR) | ||
528 | 106 | errors = True | ||
529 | 107 | elif not volume_config['device'] and not volume_config['ephemeral']: | ||
530 | 108 | # asked for permanent storage but did not define volume ID | ||
531 | 109 | hookenv.log('Ephemeral storage was requested, but there is no volume ' | ||
532 | 110 | 'defined for this unit.', hookenv.ERROR) | ||
533 | 111 | errors = True | ||
534 | 112 | |||
535 | 113 | unit_mount_name = hookenv.local_unit().replace('/', '-') | ||
536 | 114 | volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) | ||
537 | 115 | |||
538 | 116 | if errors: | ||
539 | 117 | return None | ||
540 | 118 | return volume_config | ||
541 | 119 | |||
542 | 120 | |||
543 | 121 | def mount_volume(config): | ||
544 | 122 | if os.path.exists(config['mountpoint']): | ||
545 | 123 | if not os.path.isdir(config['mountpoint']): | ||
546 | 124 | hookenv.log('Not a directory: {}'.format(config['mountpoint'])) | ||
547 | 125 | raise VolumeConfigurationError() | ||
548 | 126 | else: | ||
549 | 127 | host.mkdir(config['mountpoint']) | ||
550 | 128 | if os.path.ismount(config['mountpoint']): | ||
551 | 129 | unmount_volume(config) | ||
552 | 130 | if not host.mount(config['device'], config['mountpoint'], persist=True): | ||
553 | 131 | raise VolumeConfigurationError() | ||
554 | 132 | |||
555 | 133 | |||
556 | 134 | def unmount_volume(config): | ||
557 | 135 | if os.path.ismount(config['mountpoint']): | ||
558 | 136 | if not host.umount(config['mountpoint'], persist=True): | ||
559 | 137 | raise VolumeConfigurationError() | ||
560 | 138 | |||
561 | 139 | |||
562 | 140 | def managed_mounts(): | ||
563 | 141 | '''List of all mounted managed volumes''' | ||
564 | 142 | return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) | ||
565 | 143 | |||
566 | 144 | |||
567 | 145 | def configure_volume(before_change=lambda: None, after_change=lambda: None): | ||
568 | 146 | '''Set up storage (or don't) according to the charm's volume configuration. | ||
569 | 147 | Returns the mount point or "ephemeral". before_change and after_change | ||
570 | 148 | are optional functions to be called if the volume configuration changes. | ||
571 | 149 | ''' | ||
572 | 150 | |||
573 | 151 | config = get_config() | ||
574 | 152 | if not config: | ||
575 | 153 | hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) | ||
576 | 154 | raise VolumeConfigurationError() | ||
577 | 155 | |||
578 | 156 | if config['ephemeral']: | ||
579 | 157 | if os.path.ismount(config['mountpoint']): | ||
580 | 158 | before_change() | ||
581 | 159 | unmount_volume(config) | ||
582 | 160 | after_change() | ||
583 | 161 | return 'ephemeral' | ||
584 | 162 | else: | ||
585 | 163 | # persistent storage | ||
586 | 164 | if os.path.ismount(config['mountpoint']): | ||
587 | 165 | mounts = dict(managed_mounts()) | ||
588 | 166 | if mounts.get(config['mountpoint']) != config['device']: | ||
589 | 167 | before_change() | ||
590 | 168 | unmount_volume(config) | ||
591 | 169 | mount_volume(config) | ||
592 | 170 | after_change() | ||
593 | 171 | else: | ||
594 | 172 | before_change() | ||
595 | 173 | mount_volume(config) | ||
596 | 174 | after_change() | ||
597 | 175 | return config['mountpoint'] | ||
598 | 0 | 176 | ||
599 | === modified file 'hooks/charmhelpers/contrib/database/mysql.py' | |||
600 | --- hooks/charmhelpers/contrib/database/mysql.py 2015-02-27 10:17:54 +0000 | |||
601 | +++ hooks/charmhelpers/contrib/database/mysql.py 2015-03-03 02:26:44 +0000 | |||
602 | @@ -1,6 +1,5 @@ | |||
603 | 1 | """Helper for working with a MySQL database""" | 1 | """Helper for working with a MySQL database""" |
604 | 2 | import json | 2 | import json |
605 | 3 | import socket | ||
606 | 4 | import re | 3 | import re |
607 | 5 | import sys | 4 | import sys |
608 | 6 | import platform | 5 | import platform |
609 | @@ -22,7 +21,6 @@ | |||
610 | 22 | log, | 21 | log, |
611 | 23 | DEBUG, | 22 | DEBUG, |
612 | 24 | INFO, | 23 | INFO, |
613 | 25 | WARNING, | ||
614 | 26 | ) | 24 | ) |
615 | 27 | from charmhelpers.fetch import ( | 25 | from charmhelpers.fetch import ( |
616 | 28 | apt_install, | 26 | apt_install, |
617 | 29 | 27 | ||
618 | === added file 'hooks/charmhelpers/core/strutils.py' | |||
619 | --- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000 | |||
620 | +++ hooks/charmhelpers/core/strutils.py 2015-03-03 02:26:44 +0000 | |||
621 | @@ -0,0 +1,42 @@ | |||
622 | 1 | #!/usr/bin/env python | ||
623 | 2 | # -*- coding: utf-8 -*- | ||
624 | 3 | |||
625 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
626 | 5 | # | ||
627 | 6 | # This file is part of charm-helpers. | ||
628 | 7 | # | ||
629 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
630 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
631 | 10 | # published by the Free Software Foundation. | ||
632 | 11 | # | ||
633 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
634 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
635 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
636 | 15 | # GNU Lesser General Public License for more details. | ||
637 | 16 | # | ||
638 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
639 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
640 | 19 | |||
641 | 20 | import six | ||
642 | 21 | |||
643 | 22 | |||
644 | 23 | def bool_from_string(value): | ||
645 | 24 | """Interpret string value as boolean. | ||
646 | 25 | |||
647 | 26 | Returns True if value translates to True otherwise False. | ||
648 | 27 | """ | ||
649 | 28 | if isinstance(value, six.string_types): | ||
650 | 29 | value = six.text_type(value) | ||
651 | 30 | else: | ||
652 | 31 | msg = "Unable to interpret non-string value '%s' as boolean" % (value) | ||
653 | 32 | raise ValueError(msg) | ||
654 | 33 | |||
655 | 34 | value = value.strip().lower() | ||
656 | 35 | |||
657 | 36 | if value in ['y', 'yes', 'true', 't']: | ||
658 | 37 | return True | ||
659 | 38 | elif value in ['n', 'no', 'false', 'f']: | ||
660 | 39 | return False | ||
661 | 40 | |||
662 | 41 | msg = "Unable to interpret string value '%s' as boolean" % (value) | ||
663 | 42 | raise ValueError(msg) | ||
664 | 0 | 43 | ||
665 | === removed file 'hooks/charmhelpers/core/strutils.py' | |||
666 | --- hooks/charmhelpers/core/strutils.py 2015-02-25 20:48:34 +0000 | |||
667 | +++ hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000 | |||
668 | @@ -1,42 +0,0 @@ | |||
669 | 1 | #!/usr/bin/env python | ||
670 | 2 | # -*- coding: utf-8 -*- | ||
671 | 3 | |||
672 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
673 | 5 | # | ||
674 | 6 | # This file is part of charm-helpers. | ||
675 | 7 | # | ||
676 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
677 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
678 | 10 | # published by the Free Software Foundation. | ||
679 | 11 | # | ||
680 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
681 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
682 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
683 | 15 | # GNU Lesser General Public License for more details. | ||
684 | 16 | # | ||
685 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
686 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
687 | 19 | |||
688 | 20 | import six | ||
689 | 21 | |||
690 | 22 | |||
691 | 23 | def bool_from_string(value): | ||
692 | 24 | """Interpret string value as boolean. | ||
693 | 25 | |||
694 | 26 | Returns True if value translates to True otherwise False. | ||
695 | 27 | """ | ||
696 | 28 | if isinstance(value, six.string_types): | ||
697 | 29 | value = six.text_type(value) | ||
698 | 30 | else: | ||
699 | 31 | msg = "Unable to interpret non-string value '%s' as boolean" % (value) | ||
700 | 32 | raise ValueError(msg) | ||
701 | 33 | |||
702 | 34 | value = value.strip().lower() | ||
703 | 35 | |||
704 | 36 | if value in ['y', 'yes', 'true', 't']: | ||
705 | 37 | return True | ||
706 | 38 | elif value in ['n', 'no', 'false', 'f']: | ||
707 | 39 | return False | ||
708 | 40 | |||
709 | 41 | msg = "Unable to interpret string value '%s' as boolean" % (value) | ||
710 | 42 | raise ValueError(msg) | ||
711 | 43 | 0 | ||
712 | === added file 'hooks/charmhelpers/core/unitdata.py' | |||
713 | --- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000 | |||
714 | +++ hooks/charmhelpers/core/unitdata.py 2015-03-03 02:26:44 +0000 | |||
715 | @@ -0,0 +1,477 @@ | |||
716 | 1 | #!/usr/bin/env python | ||
717 | 2 | # -*- coding: utf-8 -*- | ||
718 | 3 | # | ||
719 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
720 | 5 | # | ||
721 | 6 | # This file is part of charm-helpers. | ||
722 | 7 | # | ||
723 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
724 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
725 | 10 | # published by the Free Software Foundation. | ||
726 | 11 | # | ||
727 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
728 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
729 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
730 | 15 | # GNU Lesser General Public License for more details. | ||
731 | 16 | # | ||
732 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
733 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
734 | 19 | # | ||
735 | 20 | # | ||
736 | 21 | # Authors: | ||
737 | 22 | # Kapil Thangavelu <kapil.foss@gmail.com> | ||
738 | 23 | # | ||
739 | 24 | """ | ||
740 | 25 | Intro | ||
741 | 26 | ----- | ||
742 | 27 | |||
743 | 28 | A simple way to store state in units. This provides a key value | ||
744 | 29 | storage with support for versioned, transactional operation, | ||
745 | 30 | and can calculate deltas from previous values to simplify unit logic | ||
746 | 31 | when processing changes. | ||
747 | 32 | |||
748 | 33 | |||
749 | 34 | Hook Integration | ||
750 | 35 | ---------------- | ||
751 | 36 | |||
752 | 37 | There are several extant frameworks for hook execution, including | ||
753 | 38 | |||
754 | 39 | - charmhelpers.core.hookenv.Hooks | ||
755 | 40 | - charmhelpers.core.services.ServiceManager | ||
756 | 41 | |||
757 | 42 | The storage classes are framework agnostic, one simple integration is | ||
758 | 43 | via the HookData contextmanager. It will record the current hook | ||
759 | 44 | execution environment (including relation data, config data, etc.), | ||
760 | 45 | setup a transaction and allow easy access to the changes from | ||
761 | 46 | previously seen values. One consequence of the integration is the | ||
762 | 47 | reservation of particular keys ('rels', 'unit', 'env', 'config', | ||
763 | 48 | 'charm_revisions') for their respective values. | ||
764 | 49 | |||
765 | 50 | Here's a fully worked integration example using hookenv.Hooks:: | ||
766 | 51 | |||
767 | 52 | from charmhelper.core import hookenv, unitdata | ||
768 | 53 | |||
769 | 54 | hook_data = unitdata.HookData() | ||
770 | 55 | db = unitdata.kv() | ||
771 | 56 | hooks = hookenv.Hooks() | ||
772 | 57 | |||
773 | 58 | @hooks.hook | ||
774 | 59 | def config_changed(): | ||
775 | 60 | # Print all changes to configuration from previously seen | ||
776 | 61 | # values. | ||
777 | 62 | for changed, (prev, cur) in hook_data.conf.items(): | ||
778 | 63 | print('config changed', changed, | ||
779 | 64 | 'previous value', prev, | ||
780 | 65 | 'current value', cur) | ||
781 | 66 | |||
782 | 67 | # Get some unit specific bookeeping | ||
783 | 68 | if not db.get('pkg_key'): | ||
784 | 69 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
785 | 70 | db.set('pkg_key', key) | ||
786 | 71 | |||
787 | 72 | # Directly access all charm config as a mapping. | ||
788 | 73 | conf = db.getrange('config', True) | ||
789 | 74 | |||
790 | 75 | # Directly access all relation data as a mapping | ||
791 | 76 | rels = db.getrange('rels', True) | ||
792 | 77 | |||
793 | 78 | if __name__ == '__main__': | ||
794 | 79 | with hook_data(): | ||
795 | 80 | hook.execute() | ||
796 | 81 | |||
797 | 82 | |||
798 | 83 | A more basic integration is via the hook_scope context manager which simply | ||
799 | 84 | manages transaction scope (and records hook name, and timestamp):: | ||
800 | 85 | |||
801 | 86 | >>> from unitdata import kv | ||
802 | 87 | >>> db = kv() | ||
803 | 88 | >>> with db.hook_scope('install'): | ||
804 | 89 | ... # do work, in transactional scope. | ||
805 | 90 | ... db.set('x', 1) | ||
806 | 91 | >>> db.get('x') | ||
807 | 92 | 1 | ||
808 | 93 | |||
809 | 94 | |||
810 | 95 | Usage | ||
811 | 96 | ----- | ||
812 | 97 | |||
813 | 98 | Values are automatically json de/serialized to preserve basic typing | ||
814 | 99 | and complex data struct capabilities (dicts, lists, ints, booleans, etc). | ||
815 | 100 | |||
816 | 101 | Individual values can be manipulated via get/set:: | ||
817 | 102 | |||
818 | 103 | >>> kv.set('y', True) | ||
819 | 104 | >>> kv.get('y') | ||
820 | 105 | True | ||
821 | 106 | |||
822 | 107 | # We can set complex values (dicts, lists) as a single key. | ||
823 | 108 | >>> kv.set('config', {'a': 1, 'b': True'}) | ||
824 | 109 | |||
825 | 110 | # Also supports returning dictionaries as a record which | ||
826 | 111 | # provides attribute access. | ||
827 | 112 | >>> config = kv.get('config', record=True) | ||
828 | 113 | >>> config.b | ||
829 | 114 | True | ||
830 | 115 | |||
831 | 116 | |||
832 | 117 | Groups of keys can be manipulated with update/getrange:: | ||
833 | 118 | |||
834 | 119 | >>> kv.update({'z': 1, 'y': 2}, prefix="gui.") | ||
835 | 120 | >>> kv.getrange('gui.', strip=True) | ||
836 | 121 | {'z': 1, 'y': 2} | ||
837 | 122 | |||
838 | 123 | When updating values, its very helpful to understand which values | ||
839 | 124 | have actually changed and how have they changed. The storage | ||
840 | 125 | provides a delta method to provide for this:: | ||
841 | 126 | |||
842 | 127 | >>> data = {'debug': True, 'option': 2} | ||
843 | 128 | >>> delta = kv.delta(data, 'config.') | ||
844 | 129 | >>> delta.debug.previous | ||
845 | 130 | None | ||
846 | 131 | >>> delta.debug.current | ||
847 | 132 | True | ||
848 | 133 | >>> delta | ||
849 | 134 | {'debug': (None, True), 'option': (None, 2)} | ||
850 | 135 | |||
851 | 136 | Note the delta method does not persist the actual change, it needs to | ||
852 | 137 | be explicitly saved via 'update' method:: | ||
853 | 138 | |||
854 | 139 | >>> kv.update(data, 'config.') | ||
855 | 140 | |||
856 | 141 | Values modified in the context of a hook scope retain historical values | ||
857 | 142 | associated to the hookname. | ||
858 | 143 | |||
859 | 144 | >>> with db.hook_scope('config-changed'): | ||
860 | 145 | ... db.set('x', 42) | ||
861 | 146 | >>> db.gethistory('x') | ||
862 | 147 | [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'), | ||
863 | 148 | (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')] | ||
864 | 149 | |||
865 | 150 | """ | ||
866 | 151 | |||
867 | 152 | import collections | ||
868 | 153 | import contextlib | ||
869 | 154 | import datetime | ||
870 | 155 | import json | ||
871 | 156 | import os | ||
872 | 157 | import pprint | ||
873 | 158 | import sqlite3 | ||
874 | 159 | import sys | ||
875 | 160 | |||
876 | 161 | __author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>' | ||
877 | 162 | |||
878 | 163 | |||
879 | 164 | class Storage(object): | ||
880 | 165 | """Simple key value database for local unit state within charms. | ||
881 | 166 | |||
882 | 167 | Modifications are automatically committed at hook exit. That's | ||
883 | 168 | currently regardless of exit code. | ||
884 | 169 | |||
885 | 170 | To support dicts, lists, integer, floats, and booleans values | ||
886 | 171 | are automatically json encoded/decoded. | ||
887 | 172 | """ | ||
888 | 173 | def __init__(self, path=None): | ||
889 | 174 | self.db_path = path | ||
890 | 175 | if path is None: | ||
891 | 176 | self.db_path = os.path.join( | ||
892 | 177 | os.environ.get('CHARM_DIR', ''), '.unit-state.db') | ||
893 | 178 | self.conn = sqlite3.connect('%s' % self.db_path) | ||
894 | 179 | self.cursor = self.conn.cursor() | ||
895 | 180 | self.revision = None | ||
896 | 181 | self._closed = False | ||
897 | 182 | self._init() | ||
898 | 183 | |||
899 | 184 | def close(self): | ||
900 | 185 | if self._closed: | ||
901 | 186 | return | ||
902 | 187 | self.flush(False) | ||
903 | 188 | self.cursor.close() | ||
904 | 189 | self.conn.close() | ||
905 | 190 | self._closed = True | ||
906 | 191 | |||
907 | 192 | def _scoped_query(self, stmt, params=None): | ||
908 | 193 | if params is None: | ||
909 | 194 | params = [] | ||
910 | 195 | return stmt, params | ||
911 | 196 | |||
912 | 197 | def get(self, key, default=None, record=False): | ||
913 | 198 | self.cursor.execute( | ||
914 | 199 | *self._scoped_query( | ||
915 | 200 | 'select data from kv where key=?', [key])) | ||
916 | 201 | result = self.cursor.fetchone() | ||
917 | 202 | if not result: | ||
918 | 203 | return default | ||
919 | 204 | if record: | ||
920 | 205 | return Record(json.loads(result[0])) | ||
921 | 206 | return json.loads(result[0]) | ||
922 | 207 | |||
923 | 208 | def getrange(self, key_prefix, strip=False): | ||
924 | 209 | stmt = "select key, data from kv where key like '%s%%'" % key_prefix | ||
925 | 210 | self.cursor.execute(*self._scoped_query(stmt)) | ||
926 | 211 | result = self.cursor.fetchall() | ||
927 | 212 | |||
928 | 213 | if not result: | ||
929 | 214 | return None | ||
930 | 215 | if not strip: | ||
931 | 216 | key_prefix = '' | ||
932 | 217 | return dict([ | ||
933 | 218 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) | ||
934 | 219 | |||
935 | 220 | def update(self, mapping, prefix=""): | ||
936 | 221 | for k, v in mapping.items(): | ||
937 | 222 | self.set("%s%s" % (prefix, k), v) | ||
938 | 223 | |||
939 | 224 | def unset(self, key): | ||
940 | 225 | self.cursor.execute('delete from kv where key=?', [key]) | ||
941 | 226 | if self.revision and self.cursor.rowcount: | ||
942 | 227 | self.cursor.execute( | ||
943 | 228 | 'insert into kv_revisions values (?, ?, ?)', | ||
944 | 229 | [key, self.revision, json.dumps('DELETED')]) | ||
945 | 230 | |||
946 | 231 | def set(self, key, value): | ||
947 | 232 | serialized = json.dumps(value) | ||
948 | 233 | |||
949 | 234 | self.cursor.execute( | ||
950 | 235 | 'select data from kv where key=?', [key]) | ||
951 | 236 | exists = self.cursor.fetchone() | ||
952 | 237 | |||
953 | 238 | # Skip mutations to the same value | ||
954 | 239 | if exists: | ||
955 | 240 | if exists[0] == serialized: | ||
956 | 241 | return value | ||
957 | 242 | |||
958 | 243 | if not exists: | ||
959 | 244 | self.cursor.execute( | ||
960 | 245 | 'insert into kv (key, data) values (?, ?)', | ||
961 | 246 | (key, serialized)) | ||
962 | 247 | else: | ||
963 | 248 | self.cursor.execute(''' | ||
964 | 249 | update kv | ||
965 | 250 | set data = ? | ||
966 | 251 | where key = ?''', [serialized, key]) | ||
967 | 252 | |||
968 | 253 | # Save | ||
969 | 254 | if not self.revision: | ||
970 | 255 | return value | ||
971 | 256 | |||
972 | 257 | self.cursor.execute( | ||
973 | 258 | 'select 1 from kv_revisions where key=? and revision=?', | ||
974 | 259 | [key, self.revision]) | ||
975 | 260 | exists = self.cursor.fetchone() | ||
976 | 261 | |||
977 | 262 | if not exists: | ||
978 | 263 | self.cursor.execute( | ||
979 | 264 | '''insert into kv_revisions ( | ||
980 | 265 | revision, key, data) values (?, ?, ?)''', | ||
981 | 266 | (self.revision, key, serialized)) | ||
982 | 267 | else: | ||
983 | 268 | self.cursor.execute( | ||
984 | 269 | ''' | ||
985 | 270 | update kv_revisions | ||
986 | 271 | set data = ? | ||
987 | 272 | where key = ? | ||
988 | 273 | and revision = ?''', | ||
989 | 274 | [serialized, key, self.revision]) | ||
990 | 275 | |||
991 | 276 | return value | ||
992 | 277 | |||
993 | 278 | def delta(self, mapping, prefix): | ||
994 | 279 | """ | ||
995 | 280 | return a delta containing values that have changed. | ||
996 | 281 | """ | ||
997 | 282 | previous = self.getrange(prefix, strip=True) | ||
998 | 283 | if not previous: | ||
999 | 284 | pk = set() | ||
1000 | 285 | else: | ||
1001 | 286 | pk = set(previous.keys()) | ||
1002 | 287 | ck = set(mapping.keys()) | ||
1003 | 288 | delta = DeltaSet() | ||
1004 | 289 | |||
1005 | 290 | # added | ||
1006 | 291 | for k in ck.difference(pk): | ||
1007 | 292 | delta[k] = Delta(None, mapping[k]) | ||
1008 | 293 | |||
1009 | 294 | # removed | ||
1010 | 295 | for k in pk.difference(ck): | ||
1011 | 296 | delta[k] = Delta(previous[k], None) | ||
1012 | 297 | |||
1013 | 298 | # changed | ||
1014 | 299 | for k in pk.intersection(ck): | ||
1015 | 300 | c = mapping[k] | ||
1016 | 301 | p = previous[k] | ||
1017 | 302 | if c != p: | ||
1018 | 303 | delta[k] = Delta(p, c) | ||
1019 | 304 | |||
1020 | 305 | return delta | ||
1021 | 306 | |||
1022 | 307 | @contextlib.contextmanager | ||
1023 | 308 | def hook_scope(self, name=""): | ||
1024 | 309 | """Scope all future interactions to the current hook execution | ||
1025 | 310 | revision.""" | ||
1026 | 311 | assert not self.revision | ||
1027 | 312 | self.cursor.execute( | ||
1028 | 313 | 'insert into hooks (hook, date) values (?, ?)', | ||
1029 | 314 | (name or sys.argv[0], | ||
1030 | 315 | datetime.datetime.utcnow().isoformat())) | ||
1031 | 316 | self.revision = self.cursor.lastrowid | ||
1032 | 317 | try: | ||
1033 | 318 | yield self.revision | ||
1034 | 319 | self.revision = None | ||
1035 | 320 | except: | ||
1036 | 321 | self.flush(False) | ||
1037 | 322 | self.revision = None | ||
1038 | 323 | raise | ||
1039 | 324 | else: | ||
1040 | 325 | self.flush() | ||
1041 | 326 | |||
1042 | 327 | def flush(self, save=True): | ||
1043 | 328 | if save: | ||
1044 | 329 | self.conn.commit() | ||
1045 | 330 | elif self._closed: | ||
1046 | 331 | return | ||
1047 | 332 | else: | ||
1048 | 333 | self.conn.rollback() | ||
1049 | 334 | |||
1050 | 335 | def _init(self): | ||
1051 | 336 | self.cursor.execute(''' | ||
1052 | 337 | create table if not exists kv ( | ||
1053 | 338 | key text, | ||
1054 | 339 | data text, | ||
1055 | 340 | primary key (key) | ||
1056 | 341 | )''') | ||
1057 | 342 | self.cursor.execute(''' | ||
1058 | 343 | create table if not exists kv_revisions ( | ||
1059 | 344 | key text, | ||
1060 | 345 | revision integer, | ||
1061 | 346 | data text, | ||
1062 | 347 | primary key (key, revision) | ||
1063 | 348 | )''') | ||
1064 | 349 | self.cursor.execute(''' | ||
1065 | 350 | create table if not exists hooks ( | ||
1066 | 351 | version integer primary key autoincrement, | ||
1067 | 352 | hook text, | ||
1068 | 353 | date text | ||
1069 | 354 | )''') | ||
1070 | 355 | self.conn.commit() | ||
1071 | 356 | |||
1072 | 357 | def gethistory(self, key, deserialize=False): | ||
1073 | 358 | self.cursor.execute( | ||
1074 | 359 | ''' | ||
1075 | 360 | select kv.revision, kv.key, kv.data, h.hook, h.date | ||
1076 | 361 | from kv_revisions kv, | ||
1077 | 362 | hooks h | ||
1078 | 363 | where kv.key=? | ||
1079 | 364 | and kv.revision = h.version | ||
1080 | 365 | ''', [key]) | ||
1081 | 366 | if deserialize is False: | ||
1082 | 367 | return self.cursor.fetchall() | ||
1083 | 368 | return map(_parse_history, self.cursor.fetchall()) | ||
1084 | 369 | |||
1085 | 370 | def debug(self, fh=sys.stderr): | ||
1086 | 371 | self.cursor.execute('select * from kv') | ||
1087 | 372 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
1088 | 373 | self.cursor.execute('select * from kv_revisions') | ||
1089 | 374 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
1090 | 375 | |||
1091 | 376 | |||
1092 | 377 | def _parse_history(d): | ||
1093 | 378 | return (d[0], d[1], json.loads(d[2]), d[3], | ||
1094 | 379 | datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f")) | ||
1095 | 380 | |||
1096 | 381 | |||
1097 | 382 | class HookData(object): | ||
1098 | 383 | """Simple integration for existing hook exec frameworks. | ||
1099 | 384 | |||
1100 | 385 | Records all unit information, and stores deltas for processing | ||
1101 | 386 | by the hook. | ||
1102 | 387 | |||
1103 | 388 | Sample:: | ||
1104 | 389 | |||
1105 | 390 | from charmhelper.core import hookenv, unitdata | ||
1106 | 391 | |||
1107 | 392 | changes = unitdata.HookData() | ||
1108 | 393 | db = unitdata.kv() | ||
1109 | 394 | hooks = hookenv.Hooks() | ||
1110 | 395 | |||
1111 | 396 | @hooks.hook | ||
1112 | 397 | def config_changed(): | ||
1113 | 398 | # View all changes to configuration | ||
1114 | 399 | for changed, (prev, cur) in changes.conf.items(): | ||
1115 | 400 | print('config changed', changed, | ||
1116 | 401 | 'previous value', prev, | ||
1117 | 402 | 'current value', cur) | ||
1118 | 403 | |||
1119 | 404 | # Get some unit specific bookeeping | ||
1120 | 405 | if not db.get('pkg_key'): | ||
1121 | 406 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
1122 | 407 | db.set('pkg_key', key) | ||
1123 | 408 | |||
1124 | 409 | if __name__ == '__main__': | ||
1125 | 410 | with changes(): | ||
1126 | 411 | hook.execute() | ||
1127 | 412 | |||
1128 | 413 | """ | ||
1129 | 414 | def __init__(self): | ||
1130 | 415 | self.kv = kv() | ||
1131 | 416 | self.conf = None | ||
1132 | 417 | self.rels = None | ||
1133 | 418 | |||
1134 | 419 | @contextlib.contextmanager | ||
1135 | 420 | def __call__(self): | ||
1136 | 421 | from charmhelpers.core import hookenv | ||
1137 | 422 | hook_name = hookenv.hook_name() | ||
1138 | 423 | |||
1139 | 424 | with self.kv.hook_scope(hook_name): | ||
1140 | 425 | self._record_charm_version(hookenv.charm_dir()) | ||
1141 | 426 | delta_config, delta_relation = self._record_hook(hookenv) | ||
1142 | 427 | yield self.kv, delta_config, delta_relation | ||
1143 | 428 | |||
1144 | 429 | def _record_charm_version(self, charm_dir): | ||
1145 | 430 | # Record revisions.. charm revisions are meaningless | ||
1146 | 431 | # to charm authors as they don't control the revision. | ||
1147 | 432 | # so logic dependnent on revision is not particularly | ||
1148 | 433 | # useful, however it is useful for debugging analysis. | ||
1149 | 434 | charm_rev = open( | ||
1150 | 435 | os.path.join(charm_dir, 'revision')).read().strip() | ||
1151 | 436 | charm_rev = charm_rev or '0' | ||
1152 | 437 | revs = self.kv.get('charm_revisions', []) | ||
1153 | 438 | if charm_rev not in revs: | ||
1154 | 439 | revs.append(charm_rev.strip() or '0') | ||
1155 | 440 | self.kv.set('charm_revisions', revs) | ||
1156 | 441 | |||
1157 | 442 | def _record_hook(self, hookenv): | ||
1158 | 443 | data = hookenv.execution_environment() | ||
1159 | 444 | self.conf = conf_delta = self.kv.delta(data['conf'], 'config') | ||
1160 | 445 | self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') | ||
1161 | 446 | self.kv.set('env', data['env']) | ||
1162 | 447 | self.kv.set('unit', data['unit']) | ||
1163 | 448 | self.kv.set('relid', data.get('relid')) | ||
1164 | 449 | return conf_delta, rels_delta | ||
1165 | 450 | |||
1166 | 451 | |||
1167 | 452 | class Record(dict): | ||
1168 | 453 | |||
1169 | 454 | __slots__ = () | ||
1170 | 455 | |||
1171 | 456 | def __getattr__(self, k): | ||
1172 | 457 | if k in self: | ||
1173 | 458 | return self[k] | ||
1174 | 459 | raise AttributeError(k) | ||
1175 | 460 | |||
1176 | 461 | |||
1177 | 462 | class DeltaSet(Record): | ||
1178 | 463 | |||
1179 | 464 | __slots__ = () | ||
1180 | 465 | |||
1181 | 466 | |||
1182 | 467 | Delta = collections.namedtuple('Delta', ['previous', 'current']) | ||
1183 | 468 | |||
1184 | 469 | |||
1185 | 470 | _KV = None | ||
1186 | 471 | |||
1187 | 472 | |||
1188 | 473 | def kv(): | ||
1189 | 474 | global _KV | ||
1190 | 475 | if _KV is None: | ||
1191 | 476 | _KV = Storage() | ||
1192 | 477 | return _KV | ||
1193 | 0 | 478 | ||
1194 | === removed file 'hooks/charmhelpers/core/unitdata.py' | |||
1195 | --- hooks/charmhelpers/core/unitdata.py 2015-02-25 20:48:34 +0000 | |||
1196 | +++ hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000 | |||
1197 | @@ -1,477 +0,0 @@ | |||
1198 | 1 | #!/usr/bin/env python | ||
1199 | 2 | # -*- coding: utf-8 -*- | ||
1200 | 3 | # | ||
1201 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
1202 | 5 | # | ||
1203 | 6 | # This file is part of charm-helpers. | ||
1204 | 7 | # | ||
1205 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1206 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1207 | 10 | # published by the Free Software Foundation. | ||
1208 | 11 | # | ||
1209 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
1210 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1211 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1212 | 15 | # GNU Lesser General Public License for more details. | ||
1213 | 16 | # | ||
1214 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
1215 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1216 | 19 | # | ||
1217 | 20 | # | ||
1218 | 21 | # Authors: | ||
1219 | 22 | # Kapil Thangavelu <kapil.foss@gmail.com> | ||
1220 | 23 | # | ||
1221 | 24 | """ | ||
1222 | 25 | Intro | ||
1223 | 26 | ----- | ||
1224 | 27 | |||
1225 | 28 | A simple way to store state in units. This provides a key value | ||
1226 | 29 | storage with support for versioned, transactional operation, | ||
1227 | 30 | and can calculate deltas from previous values to simplify unit logic | ||
1228 | 31 | when processing changes. | ||
1229 | 32 | |||
1230 | 33 | |||
1231 | 34 | Hook Integration | ||
1232 | 35 | ---------------- | ||
1233 | 36 | |||
1234 | 37 | There are several extant frameworks for hook execution, including | ||
1235 | 38 | |||
1236 | 39 | - charmhelpers.core.hookenv.Hooks | ||
1237 | 40 | - charmhelpers.core.services.ServiceManager | ||
1238 | 41 | |||
1239 | 42 | The storage classes are framework agnostic, one simple integration is | ||
1240 | 43 | via the HookData contextmanager. It will record the current hook | ||
1241 | 44 | execution environment (including relation data, config data, etc.), | ||
1242 | 45 | setup a transaction and allow easy access to the changes from | ||
1243 | 46 | previously seen values. One consequence of the integration is the | ||
1244 | 47 | reservation of particular keys ('rels', 'unit', 'env', 'config', | ||
1245 | 48 | 'charm_revisions') for their respective values. | ||
1246 | 49 | |||
1247 | 50 | Here's a fully worked integration example using hookenv.Hooks:: | ||
1248 | 51 | |||
1249 | 52 | from charmhelper.core import hookenv, unitdata | ||
1250 | 53 | |||
1251 | 54 | hook_data = unitdata.HookData() | ||
1252 | 55 | db = unitdata.kv() | ||
1253 | 56 | hooks = hookenv.Hooks() | ||
1254 | 57 | |||
1255 | 58 | @hooks.hook | ||
1256 | 59 | def config_changed(): | ||
1257 | 60 | # Print all changes to configuration from previously seen | ||
1258 | 61 | # values. | ||
1259 | 62 | for changed, (prev, cur) in hook_data.conf.items(): | ||
1260 | 63 | print('config changed', changed, | ||
1261 | 64 | 'previous value', prev, | ||
1262 | 65 | 'current value', cur) | ||
1263 | 66 | |||
1264 | 67 | # Get some unit specific bookeeping | ||
1265 | 68 | if not db.get('pkg_key'): | ||
1266 | 69 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
1267 | 70 | db.set('pkg_key', key) | ||
1268 | 71 | |||
1269 | 72 | # Directly access all charm config as a mapping. | ||
1270 | 73 | conf = db.getrange('config', True) | ||
1271 | 74 | |||
1272 | 75 | # Directly access all relation data as a mapping | ||
1273 | 76 | rels = db.getrange('rels', True) | ||
1274 | 77 | |||
1275 | 78 | if __name__ == '__main__': | ||
1276 | 79 | with hook_data(): | ||
1277 | 80 | hook.execute() | ||
1278 | 81 | |||
1279 | 82 | |||
1280 | 83 | A more basic integration is via the hook_scope context manager which simply | ||
1281 | 84 | manages transaction scope (and records hook name, and timestamp):: | ||
1282 | 85 | |||
1283 | 86 | >>> from unitdata import kv | ||
1284 | 87 | >>> db = kv() | ||
1285 | 88 | >>> with db.hook_scope('install'): | ||
1286 | 89 | ... # do work, in transactional scope. | ||
1287 | 90 | ... db.set('x', 1) | ||
1288 | 91 | >>> db.get('x') | ||
1289 | 92 | 1 | ||
1290 | 93 | |||
1291 | 94 | |||
1292 | 95 | Usage | ||
1293 | 96 | ----- | ||
1294 | 97 | |||
1295 | 98 | Values are automatically json de/serialized to preserve basic typing | ||
1296 | 99 | and complex data struct capabilities (dicts, lists, ints, booleans, etc). | ||
1297 | 100 | |||
1298 | 101 | Individual values can be manipulated via get/set:: | ||
1299 | 102 | |||
1300 | 103 | >>> kv.set('y', True) | ||
1301 | 104 | >>> kv.get('y') | ||
1302 | 105 | True | ||
1303 | 106 | |||
1304 | 107 | # We can set complex values (dicts, lists) as a single key. | ||
1305 | 108 | >>> kv.set('config', {'a': 1, 'b': True'}) | ||
1306 | 109 | |||
1307 | 110 | # Also supports returning dictionaries as a record which | ||
1308 | 111 | # provides attribute access. | ||
1309 | 112 | >>> config = kv.get('config', record=True) | ||
1310 | 113 | >>> config.b | ||
1311 | 114 | True | ||
1312 | 115 | |||
1313 | 116 | |||
1314 | 117 | Groups of keys can be manipulated with update/getrange:: | ||
1315 | 118 | |||
1316 | 119 | >>> kv.update({'z': 1, 'y': 2}, prefix="gui.") | ||
1317 | 120 | >>> kv.getrange('gui.', strip=True) | ||
1318 | 121 | {'z': 1, 'y': 2} | ||
1319 | 122 | |||
1320 | 123 | When updating values, its very helpful to understand which values | ||
1321 | 124 | have actually changed and how have they changed. The storage | ||
1322 | 125 | provides a delta method to provide for this:: | ||
1323 | 126 | |||
1324 | 127 | >>> data = {'debug': True, 'option': 2} | ||
1325 | 128 | >>> delta = kv.delta(data, 'config.') | ||
1326 | 129 | >>> delta.debug.previous | ||
1327 | 130 | None | ||
1328 | 131 | >>> delta.debug.current | ||
1329 | 132 | True | ||
1330 | 133 | >>> delta | ||
1331 | 134 | {'debug': (None, True), 'option': (None, 2)} | ||
1332 | 135 | |||
1333 | 136 | Note the delta method does not persist the actual change, it needs to | ||
1334 | 137 | be explicitly saved via 'update' method:: | ||
1335 | 138 | |||
1336 | 139 | >>> kv.update(data, 'config.') | ||
1337 | 140 | |||
1338 | 141 | Values modified in the context of a hook scope retain historical values | ||
1339 | 142 | associated to the hookname. | ||
1340 | 143 | |||
1341 | 144 | >>> with db.hook_scope('config-changed'): | ||
1342 | 145 | ... db.set('x', 42) | ||
1343 | 146 | >>> db.gethistory('x') | ||
1344 | 147 | [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'), | ||
1345 | 148 | (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')] | ||
1346 | 149 | |||
1347 | 150 | """ | ||
1348 | 151 | |||
1349 | 152 | import collections | ||
1350 | 153 | import contextlib | ||
1351 | 154 | import datetime | ||
1352 | 155 | import json | ||
1353 | 156 | import os | ||
1354 | 157 | import pprint | ||
1355 | 158 | import sqlite3 | ||
1356 | 159 | import sys | ||
1357 | 160 | |||
1358 | 161 | __author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>' | ||
1359 | 162 | |||
1360 | 163 | |||
1361 | 164 | class Storage(object): | ||
1362 | 165 | """Simple key value database for local unit state within charms. | ||
1363 | 166 | |||
1364 | 167 | Modifications are automatically committed at hook exit. That's | ||
1365 | 168 | currently regardless of exit code. | ||
1366 | 169 | |||
1367 | 170 | To support dicts, lists, integer, floats, and booleans values | ||
1368 | 171 | are automatically json encoded/decoded. | ||
1369 | 172 | """ | ||
1370 | 173 | def __init__(self, path=None): | ||
1371 | 174 | self.db_path = path | ||
1372 | 175 | if path is None: | ||
1373 | 176 | self.db_path = os.path.join( | ||
1374 | 177 | os.environ.get('CHARM_DIR', ''), '.unit-state.db') | ||
1375 | 178 | self.conn = sqlite3.connect('%s' % self.db_path) | ||
1376 | 179 | self.cursor = self.conn.cursor() | ||
1377 | 180 | self.revision = None | ||
1378 | 181 | self._closed = False | ||
1379 | 182 | self._init() | ||
1380 | 183 | |||
1381 | 184 | def close(self): | ||
1382 | 185 | if self._closed: | ||
1383 | 186 | return | ||
1384 | 187 | self.flush(False) | ||
1385 | 188 | self.cursor.close() | ||
1386 | 189 | self.conn.close() | ||
1387 | 190 | self._closed = True | ||
1388 | 191 | |||
1389 | 192 | def _scoped_query(self, stmt, params=None): | ||
1390 | 193 | if params is None: | ||
1391 | 194 | params = [] | ||
1392 | 195 | return stmt, params | ||
1393 | 196 | |||
1394 | 197 | def get(self, key, default=None, record=False): | ||
1395 | 198 | self.cursor.execute( | ||
1396 | 199 | *self._scoped_query( | ||
1397 | 200 | 'select data from kv where key=?', [key])) | ||
1398 | 201 | result = self.cursor.fetchone() | ||
1399 | 202 | if not result: | ||
1400 | 203 | return default | ||
1401 | 204 | if record: | ||
1402 | 205 | return Record(json.loads(result[0])) | ||
1403 | 206 | return json.loads(result[0]) | ||
1404 | 207 | |||
1405 | 208 | def getrange(self, key_prefix, strip=False): | ||
1406 | 209 | stmt = "select key, data from kv where key like '%s%%'" % key_prefix | ||
1407 | 210 | self.cursor.execute(*self._scoped_query(stmt)) | ||
1408 | 211 | result = self.cursor.fetchall() | ||
1409 | 212 | |||
1410 | 213 | if not result: | ||
1411 | 214 | return None | ||
1412 | 215 | if not strip: | ||
1413 | 216 | key_prefix = '' | ||
1414 | 217 | return dict([ | ||
1415 | 218 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) | ||
1416 | 219 | |||
1417 | 220 | def update(self, mapping, prefix=""): | ||
1418 | 221 | for k, v in mapping.items(): | ||
1419 | 222 | self.set("%s%s" % (prefix, k), v) | ||
1420 | 223 | |||
1421 | 224 | def unset(self, key): | ||
1422 | 225 | self.cursor.execute('delete from kv where key=?', [key]) | ||
1423 | 226 | if self.revision and self.cursor.rowcount: | ||
1424 | 227 | self.cursor.execute( | ||
1425 | 228 | 'insert into kv_revisions values (?, ?, ?)', | ||
1426 | 229 | [key, self.revision, json.dumps('DELETED')]) | ||
1427 | 230 | |||
1428 | 231 | def set(self, key, value): | ||
1429 | 232 | serialized = json.dumps(value) | ||
1430 | 233 | |||
1431 | 234 | self.cursor.execute( | ||
1432 | 235 | 'select data from kv where key=?', [key]) | ||
1433 | 236 | exists = self.cursor.fetchone() | ||
1434 | 237 | |||
1435 | 238 | # Skip mutations to the same value | ||
1436 | 239 | if exists: | ||
1437 | 240 | if exists[0] == serialized: | ||
1438 | 241 | return value | ||
1439 | 242 | |||
1440 | 243 | if not exists: | ||
1441 | 244 | self.cursor.execute( | ||
1442 | 245 | 'insert into kv (key, data) values (?, ?)', | ||
1443 | 246 | (key, serialized)) | ||
1444 | 247 | else: | ||
1445 | 248 | self.cursor.execute(''' | ||
1446 | 249 | update kv | ||
1447 | 250 | set data = ? | ||
1448 | 251 | where key = ?''', [serialized, key]) | ||
1449 | 252 | |||
1450 | 253 | # Save | ||
1451 | 254 | if not self.revision: | ||
1452 | 255 | return value | ||
1453 | 256 | |||
1454 | 257 | self.cursor.execute( | ||
1455 | 258 | 'select 1 from kv_revisions where key=? and revision=?', | ||
1456 | 259 | [key, self.revision]) | ||
1457 | 260 | exists = self.cursor.fetchone() | ||
1458 | 261 | |||
1459 | 262 | if not exists: | ||
1460 | 263 | self.cursor.execute( | ||
1461 | 264 | '''insert into kv_revisions ( | ||
1462 | 265 | revision, key, data) values (?, ?, ?)''', | ||
1463 | 266 | (self.revision, key, serialized)) | ||
1464 | 267 | else: | ||
1465 | 268 | self.cursor.execute( | ||
1466 | 269 | ''' | ||
1467 | 270 | update kv_revisions | ||
1468 | 271 | set data = ? | ||
1469 | 272 | where key = ? | ||
1470 | 273 | and revision = ?''', | ||
1471 | 274 | [serialized, key, self.revision]) | ||
1472 | 275 | |||
1473 | 276 | return value | ||
1474 | 277 | |||
1475 | 278 | def delta(self, mapping, prefix): | ||
1476 | 279 | """ | ||
1477 | 280 | return a delta containing values that have changed. | ||
1478 | 281 | """ | ||
1479 | 282 | previous = self.getrange(prefix, strip=True) | ||
1480 | 283 | if not previous: | ||
1481 | 284 | pk = set() | ||
1482 | 285 | else: | ||
1483 | 286 | pk = set(previous.keys()) | ||
1484 | 287 | ck = set(mapping.keys()) | ||
1485 | 288 | delta = DeltaSet() | ||
1486 | 289 | |||
1487 | 290 | # added | ||
1488 | 291 | for k in ck.difference(pk): | ||
1489 | 292 | delta[k] = Delta(None, mapping[k]) | ||
1490 | 293 | |||
1491 | 294 | # removed | ||
1492 | 295 | for k in pk.difference(ck): | ||
1493 | 296 | delta[k] = Delta(previous[k], None) | ||
1494 | 297 | |||
1495 | 298 | # changed | ||
1496 | 299 | for k in pk.intersection(ck): | ||
1497 | 300 | c = mapping[k] | ||
1498 | 301 | p = previous[k] | ||
1499 | 302 | if c != p: | ||
1500 | 303 | delta[k] = Delta(p, c) | ||
1501 | 304 | |||
1502 | 305 | return delta | ||
1503 | 306 | |||
1504 | 307 | @contextlib.contextmanager | ||
1505 | 308 | def hook_scope(self, name=""): | ||
1506 | 309 | """Scope all future interactions to the current hook execution | ||
1507 | 310 | revision.""" | ||
1508 | 311 | assert not self.revision | ||
1509 | 312 | self.cursor.execute( | ||
1510 | 313 | 'insert into hooks (hook, date) values (?, ?)', | ||
1511 | 314 | (name or sys.argv[0], | ||
1512 | 315 | datetime.datetime.utcnow().isoformat())) | ||
1513 | 316 | self.revision = self.cursor.lastrowid | ||
1514 | 317 | try: | ||
1515 | 318 | yield self.revision | ||
1516 | 319 | self.revision = None | ||
1517 | 320 | except: | ||
1518 | 321 | self.flush(False) | ||
1519 | 322 | self.revision = None | ||
1520 | 323 | raise | ||
1521 | 324 | else: | ||
1522 | 325 | self.flush() | ||
1523 | 326 | |||
1524 | 327 | def flush(self, save=True): | ||
1525 | 328 | if save: | ||
1526 | 329 | self.conn.commit() | ||
1527 | 330 | elif self._closed: | ||
1528 | 331 | return | ||
1529 | 332 | else: | ||
1530 | 333 | self.conn.rollback() | ||
1531 | 334 | |||
1532 | 335 | def _init(self): | ||
1533 | 336 | self.cursor.execute(''' | ||
1534 | 337 | create table if not exists kv ( | ||
1535 | 338 | key text, | ||
1536 | 339 | data text, | ||
1537 | 340 | primary key (key) | ||
1538 | 341 | )''') | ||
1539 | 342 | self.cursor.execute(''' | ||
1540 | 343 | create table if not exists kv_revisions ( | ||
1541 | 344 | key text, | ||
1542 | 345 | revision integer, | ||
1543 | 346 | data text, | ||
1544 | 347 | primary key (key, revision) | ||
1545 | 348 | )''') | ||
1546 | 349 | self.cursor.execute(''' | ||
1547 | 350 | create table if not exists hooks ( | ||
1548 | 351 | version integer primary key autoincrement, | ||
1549 | 352 | hook text, | ||
1550 | 353 | date text | ||
1551 | 354 | )''') | ||
1552 | 355 | self.conn.commit() | ||
1553 | 356 | |||
1554 | 357 | def gethistory(self, key, deserialize=False): | ||
1555 | 358 | self.cursor.execute( | ||
1556 | 359 | ''' | ||
1557 | 360 | select kv.revision, kv.key, kv.data, h.hook, h.date | ||
1558 | 361 | from kv_revisions kv, | ||
1559 | 362 | hooks h | ||
1560 | 363 | where kv.key=? | ||
1561 | 364 | and kv.revision = h.version | ||
1562 | 365 | ''', [key]) | ||
1563 | 366 | if deserialize is False: | ||
1564 | 367 | return self.cursor.fetchall() | ||
1565 | 368 | return map(_parse_history, self.cursor.fetchall()) | ||
1566 | 369 | |||
1567 | 370 | def debug(self, fh=sys.stderr): | ||
1568 | 371 | self.cursor.execute('select * from kv') | ||
1569 | 372 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
1570 | 373 | self.cursor.execute('select * from kv_revisions') | ||
1571 | 374 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
1572 | 375 | |||
1573 | 376 | |||
1574 | 377 | def _parse_history(d): | ||
1575 | 378 | return (d[0], d[1], json.loads(d[2]), d[3], | ||
1576 | 379 | datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f")) | ||
1577 | 380 | |||
1578 | 381 | |||
1579 | 382 | class HookData(object): | ||
1580 | 383 | """Simple integration for existing hook exec frameworks. | ||
1581 | 384 | |||
1582 | 385 | Records all unit information, and stores deltas for processing | ||
1583 | 386 | by the hook. | ||
1584 | 387 | |||
1585 | 388 | Sample:: | ||
1586 | 389 | |||
1587 | 390 | from charmhelper.core import hookenv, unitdata | ||
1588 | 391 | |||
1589 | 392 | changes = unitdata.HookData() | ||
1590 | 393 | db = unitdata.kv() | ||
1591 | 394 | hooks = hookenv.Hooks() | ||
1592 | 395 | |||
1593 | 396 | @hooks.hook | ||
1594 | 397 | def config_changed(): | ||
1595 | 398 | # View all changes to configuration | ||
1596 | 399 | for changed, (prev, cur) in changes.conf.items(): | ||
1597 | 400 | print('config changed', changed, | ||
1598 | 401 | 'previous value', prev, | ||
1599 | 402 | 'current value', cur) | ||
1600 | 403 | |||
1601 | 404 | # Get some unit specific bookeeping | ||
1602 | 405 | if not db.get('pkg_key'): | ||
1603 | 406 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
1604 | 407 | db.set('pkg_key', key) | ||
1605 | 408 | |||
1606 | 409 | if __name__ == '__main__': | ||
1607 | 410 | with changes(): | ||
1608 | 411 | hook.execute() | ||
1609 | 412 | |||
1610 | 413 | """ | ||
1611 | 414 | def __init__(self): | ||
1612 | 415 | self.kv = kv() | ||
1613 | 416 | self.conf = None | ||
1614 | 417 | self.rels = None | ||
1615 | 418 | |||
1616 | 419 | @contextlib.contextmanager | ||
1617 | 420 | def __call__(self): | ||
1618 | 421 | from charmhelpers.core import hookenv | ||
1619 | 422 | hook_name = hookenv.hook_name() | ||
1620 | 423 | |||
1621 | 424 | with self.kv.hook_scope(hook_name): | ||
1622 | 425 | self._record_charm_version(hookenv.charm_dir()) | ||
1623 | 426 | delta_config, delta_relation = self._record_hook(hookenv) | ||
1624 | 427 | yield self.kv, delta_config, delta_relation | ||
1625 | 428 | |||
1626 | 429 | def _record_charm_version(self, charm_dir): | ||
1627 | 430 | # Record revisions.. charm revisions are meaningless | ||
1628 | 431 | # to charm authors as they don't control the revision. | ||
1629 | 432 | # so logic dependnent on revision is not particularly | ||
1630 | 433 | # useful, however it is useful for debugging analysis. | ||
1631 | 434 | charm_rev = open( | ||
1632 | 435 | os.path.join(charm_dir, 'revision')).read().strip() | ||
1633 | 436 | charm_rev = charm_rev or '0' | ||
1634 | 437 | revs = self.kv.get('charm_revisions', []) | ||
1635 | 438 | if charm_rev not in revs: | ||
1636 | 439 | revs.append(charm_rev.strip() or '0') | ||
1637 | 440 | self.kv.set('charm_revisions', revs) | ||
1638 | 441 | |||
1639 | 442 | def _record_hook(self, hookenv): | ||
1640 | 443 | data = hookenv.execution_environment() | ||
1641 | 444 | self.conf = conf_delta = self.kv.delta(data['conf'], 'config') | ||
1642 | 445 | self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') | ||
1643 | 446 | self.kv.set('env', data['env']) | ||
1644 | 447 | self.kv.set('unit', data['unit']) | ||
1645 | 448 | self.kv.set('relid', data.get('relid')) | ||
1646 | 449 | return conf_delta, rels_delta | ||
1647 | 450 | |||
1648 | 451 | |||
1649 | 452 | class Record(dict): | ||
1650 | 453 | |||
1651 | 454 | __slots__ = () | ||
1652 | 455 | |||
1653 | 456 | def __getattr__(self, k): | ||
1654 | 457 | if k in self: | ||
1655 | 458 | return self[k] | ||
1656 | 459 | raise AttributeError(k) | ||
1657 | 460 | |||
1658 | 461 | |||
1659 | 462 | class DeltaSet(Record): | ||
1660 | 463 | |||
1661 | 464 | __slots__ = () | ||
1662 | 465 | |||
1663 | 466 | |||
1664 | 467 | Delta = collections.namedtuple('Delta', ['previous', 'current']) | ||
1665 | 468 | |||
1666 | 469 | |||
1667 | 470 | _KV = None | ||
1668 | 471 | |||
1669 | 472 | |||
1670 | 473 | def kv(): | ||
1671 | 474 | global _KV | ||
1672 | 475 | if _KV is None: | ||
1673 | 476 | _KV = Storage() | ||
1674 | 477 | return _KV | ||
1675 | 478 | 0 | ||
1676 | === added symlink 'hooks/nrpe-external-master-relation-changed' | |||
1677 | === target is u'percona_hooks.py' | |||
1678 | === added symlink 'hooks/nrpe-external-master-relation-joined' | |||
1679 | === target is u'percona_hooks.py' | |||
1680 | === modified file 'hooks/percona_hooks.py' | |||
1681 | --- hooks/percona_hooks.py 2015-02-16 14:12:42 +0000 | |||
1682 | +++ hooks/percona_hooks.py 2015-03-03 02:26:44 +0000 | |||
1683 | @@ -69,6 +69,8 @@ | |||
1684 | 69 | is_address_in_network, | 69 | is_address_in_network, |
1685 | 70 | ) | 70 | ) |
1686 | 71 | 71 | ||
1687 | 72 | from charmhelpers.contrib.charmsupport import nrpe | ||
1688 | 73 | |||
1689 | 72 | hooks = Hooks() | 74 | hooks = Hooks() |
1690 | 73 | 75 | ||
1691 | 74 | LEADER_RES = 'grp_percona_cluster' | 76 | LEADER_RES = 'grp_percona_cluster' |
1692 | @@ -426,6 +428,23 @@ | |||
1693 | 426 | relation_clear(r_id) | 428 | relation_clear(r_id) |
1694 | 427 | 429 | ||
1695 | 428 | 430 | ||
1696 | 431 | @hooks.hook('nrpe-external-master-relation-joined', | ||
1697 | 432 | 'nrpe-external-master-relation-changed') | ||
1698 | 433 | def update_nrpe_config(): | ||
1699 | 434 | # python-dbus is used by check_upstart_job | ||
1700 | 435 | apt_install('python-dbus') | ||
1701 | 436 | hostname = nrpe.get_nagios_hostname() | ||
1702 | 437 | current_unit = nrpe.get_nagios_unit_name() | ||
1703 | 438 | nrpe_setup = nrpe.NRPE(hostname=hostname) | ||
1704 | 439 | nrpe.add_init_service_checks(nrpe_setup, 'mysql', current_unit) | ||
1705 | 440 | nrpe_setup.add_check( | ||
1706 | 441 | shortname='mysql_proc', | ||
1707 | 442 | description='Check MySQL process {%s}' % current_unit, | ||
1708 | 443 | check_cmd='check_procs -c 1:1 -C mysqld' | ||
1709 | 444 | ) | ||
1710 | 445 | nrpe_setup.write() | ||
1711 | 446 | |||
1712 | 447 | |||
1713 | 429 | def main(): | 448 | def main(): |
1714 | 430 | try: | 449 | try: |
1715 | 431 | hooks.execute(sys.argv) | 450 | hooks.execute(sys.argv) |
update_ nrpe_config( ) never seems to get called. The nrpe-external- master- relation- {joined, changed} symlinks are missing and update_ nrpe_config( ) is not called from anyother hooks