Merge lp:~brad-marshall/charms/trusty/ntp/add-nrpe-checks into lp:charms/trusty/ntp

Proposed by Brad Marshall
Status: Merged
Merged at revision: 18
Proposed branch: lp:~brad-marshall/charms/trusty/ntp/add-nrpe-checks
Merge into: lp:charms/trusty/ntp
Diff against target: 3925 lines (+3087/-116)
29 files modified
charm-helpers-sync.yaml (+1/-0)
config.yaml (+23/-1)
files/nagios/check_ntpd.pl (+154/-0)
files/nagios/check_ntpmon.py (+361/-0)
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
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/templating/__init__.py (+15/-0)
hooks/charmhelpers/contrib/templating/jinja.py (+25/-9)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/fstab.py (+30/-12)
hooks/charmhelpers/core/hookenv.py (+100/-19)
hooks/charmhelpers/core/host.py (+127/-37)
hooks/charmhelpers/core/services/__init__.py (+18/-0)
hooks/charmhelpers/core/services/base.py (+329/-0)
hooks/charmhelpers/core/services/helpers.py (+267/-0)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/templating.py (+68/-0)
hooks/charmhelpers/core/unitdata.py (+477/-0)
hooks/charmhelpers/fetch/__init__.py (+61/-18)
hooks/charmhelpers/fetch/archiveurl.py (+115/-17)
hooks/charmhelpers/fetch/bzrurl.py (+30/-2)
hooks/charmhelpers/fetch/giturl.py (+71/-0)
hooks/ntp_hooks.py (+39/-0)
metadata.yaml (+5/-1)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/ntp/add-nrpe-checks
Reviewer Review Type Date Requested Status
Marco Ceppi (community) Approve
Review via email: mp+253156@code.launchpad.net

Description of the change

Adds some basic nrpe checks for ntp.

To post a comment you must log in.
24. By Brad Marshall

[bradm] Add ntpmon checks from paulgear

25. By Brad Marshall

[bradm] Adding missed string

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

LGTM +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers-sync.yaml'
2--- charm-helpers-sync.yaml 2014-08-05 05:12:15 +0000
3+++ charm-helpers-sync.yaml 2015-03-23 00:51:45 +0000
4@@ -4,3 +4,4 @@
5 - core
6 - fetch
7 - contrib.templating.jinja
8+ - contrib.charmsupport
9
10=== modified file 'config.yaml'
11--- config.yaml 2014-06-20 14:08:13 +0000
12+++ config.yaml 2015-03-23 00:51:45 +0000
13@@ -1,5 +1,27 @@
14 options:
15 source:
16- default:
17+ default: ""
18 type: string
19 description: Space separated list of NTP servers to use as source for time.
20+ nagios_context:
21+ default: "juju"
22+ type: string
23+ description: |
24+ Used by the nrpe-external-master subordinate charm.
25+ A string that will be prepended to instance name to set the host name
26+ in nagios. So for instance the hostname would be something like:
27+ juju-myservice-0
28+ If you're running multiple environments with the same services in them
29+ this allows you to differentiate between them.
30+ nagios_servicegroups:
31+ default: ""
32+ type: string
33+ description: |
34+ A comma-separated list of nagios servicegroups.
35+ If left empty, the nagios_context will be used as the servicegroup
36+ nagios_ntpmon_checks:
37+ default: "offset peers reachability sync"
38+ type: string
39+ description: |
40+ A space-seperated list of nagios ntpmon checks to enable.
41+ If left empty, no ntpmon checks will be used.
42
43=== added directory 'files'
44=== added directory 'files/nagios'
45=== added file 'files/nagios/check_ntpd.pl'
46--- files/nagios/check_ntpd.pl 1970-01-01 00:00:00 +0000
47+++ files/nagios/check_ntpd.pl 2015-03-23 00:51:45 +0000
48@@ -0,0 +1,154 @@
49+#!/usr/bin/perl -w
50+# Script from http://exchange.nagios.org/directory/Plugins/Network-Protocols/NTP-and-Time/check_ntpd/details
51+
52+use Getopt::Long;
53+use strict;
54+
55+GetOptions(
56+ "critical=i" => \(my $critical_threshold = '50'),
57+ "warning=i" => \(my $warning_threshold = '75'),
58+ "peer_critical=i" => \(my $peer_critical_threshold = '1'),
59+ "peer_warning=i" => \(my $peer_warning_threshold = '2'),
60+ "help" => \&display_help,
61+);
62+
63+my $ntpq_path = `/usr/bin/which ntpq`;
64+$ntpq_path =~ s/\n//g;
65+my @server_list = `$ntpq_path -pn`;
66+my %server_health;
67+my $peer_count;
68+my $overall_health = 0;
69+my $good_count;
70+my $selected_primary;
71+my $selected_backup = 0;
72+
73+# Cleanup server list
74+for(my $i = 0; $i < @server_list; $i++) {
75+ if($server_list[$i] =~ /LOCAL/) {
76+ splice(@server_list, $i, 1);
77+ $i--;
78+ } elsif($server_list[$i] =~ /^===/) {
79+ splice(@server_list, $i, 1);
80+ $i--;
81+ } elsif($server_list[$i] =~ /jitter$/) {
82+ splice(@server_list, $i, 1);
83+ $i--;
84+ } elsif($server_list[$i] =~ /^No association/) {
85+ splice(@server_list, $i, 1);
86+ $i--;
87+ }
88+}
89+
90+# Get number of peers
91+$peer_count = @server_list;
92+
93+# Cycle through peers
94+for(my $i = 0; $i < @server_list; $i++) {
95+ #split each element of the peer line
96+ my @tmp_array = split(" ", $server_list[$i]);
97+
98+ # Check for first character of peer
99+ # space = Discarded due to high stratum and/or failed sanity checks.
100+ # x = Designated falseticker by the intersection algorithm.
101+ # . = Culled from the end of the candidate list.
102+ # - = Discarded by the clustering algorithm.
103+ # + = Included in the final selection set.
104+ # # = Selected for synchronization but distance exceeds maximum.
105+ # * = Selected for synchronization.
106+ # o = Selected for synchronization, pps signal in use.
107+ if(substr($tmp_array[0], 0, 1) eq '*') {
108+ $selected_primary = "true";
109+ } elsif(substr($tmp_array[0], 0, 1) eq '+') {
110+ $selected_backup++;
111+ }
112+
113+ $good_count = 0;
114+ # Read in the octal number in column 6
115+ my $rearch = oct($tmp_array[6]);
116+
117+ # while $rearch is not 0
118+ while($rearch) {
119+ # 1s place 0 or 1?
120+ $good_count += $rearch % 2;
121+ # Bit shift to the right
122+ $rearch = $rearch >> 1;
123+ }
124+
125+ # Calculate good packets received
126+ $rearch = int(($good_count / 8) * 100);
127+
128+ # Set percentage in hash
129+ $server_health{$tmp_array[0]} = $rearch;
130+}
131+
132+# Cycle through hash and tally weighted average of peer health
133+while(my($key, $val) = each(%server_health)) {
134+ $overall_health += $val * (1 / $peer_count);
135+}
136+
137+########################### Nagios Status checks ###########################
138+#if overall health is below critical threshold, crit
139+if($overall_health <= $critical_threshold) {
140+ print_overall_health("Critical");
141+ print_server_list();
142+ exit 2;
143+}
144+
145+#if overall health is below warning and above critical threshold, warn
146+if(($overall_health <= $warning_threshold) && ($overall_health > $critical_threshold)) {
147+ print_overall_health("Warning");
148+ print_server_list();
149+ exit 1;
150+}
151+
152+#if the number of peers is below the critical threshold, crit
153+if($peer_count <= $peer_critical_threshold) {
154+ print_overall_health("Critical");
155+ print_server_list();
156+ exit 2;
157+#if the number of peers is below the warning threshold, warn
158+} elsif($peer_count <= $peer_warning_threshold) {
159+ print_overall_health("Warning");
160+ print_server_list();
161+ exit 1;
162+}
163+
164+#check to make sure we have one backup and one selected ntp server
165+#if there is no primary ntp server selected, crit
166+if($selected_primary ne "true") {
167+ print_overall_health("Critical");
168+ print_server_list();
169+ exit 2;
170+#if there is no backup ntp server selected, warn
171+} elsif($selected_backup < 1) {
172+ print_overall_health("Warning");
173+ print_server_list();
174+ exit 1;
175+}
176+
177+print_overall_health("OK");
178+print_server_list();
179+exit 0;
180+
181+sub print_server_list {
182+ print "---------------------------\n";
183+ while(my($key, $val) = each(%server_health)) {
184+ print "Received " . $val . "% of the traffic from " . $key . "\n";
185+ }
186+}
187+
188+sub print_overall_health {
189+ print $_[0] . " - NTPd Health is " . $overall_health . "% with " . $peer_count . " peers.\n";
190+}
191+
192+sub display_help {
193+ print "This nagios check is to determine the health of the NTPd client on the local system. It uses the reach attribute from 'ntpq -pn' to determine the health of each listed peer, and determines the average health based on the number of peers. For example, if there are 3 peers, and one peer has dropped 2 of the last 8 packets, it's health will be 75%. This will result in an overall health of about 92% ((100+100+75) / 3).\n";
194+ print "\n";
195+ print "Available Options:\n";
196+ print "\t--critical|-c <num>\t-Set the critical threshold for overall health (default:50)\n";
197+ print "\t--warning|-w <num>\t-Set the warning threshold for overall health (default:75)\n";
198+ print "\t--peer_critical <num>\t-Set the critical threshold for number of peers (default:1)\n";
199+ print "\t--peer_warning <num>\t-Set the warning threshold for number of peers (default:2)\n";
200+ print "\t--help|-h\t\t-display this help\n";
201+ exit 0;
202+}
203
204=== added file 'files/nagios/check_ntpmon.py'
205--- files/nagios/check_ntpmon.py 1970-01-01 00:00:00 +0000
206+++ files/nagios/check_ntpmon.py 2015-03-23 00:51:45 +0000
207@@ -0,0 +1,361 @@
208+#!/usr/bin/python
209+#
210+# Author: Paul Gear
211+# Copyright: (c) 2015 Gear Consulting Pty Ltd <http://libertysys.com.au/>
212+# License: GPLv3 <http://www.gnu.org/licenses/gpl.html>
213+# Description: NTP metrics as a Nagios check.
214+#
215+# This program is free software: you can redistribute it and/or modify it under
216+# the terms of the GNU General Public License as published by the Free Software
217+# Foundation, either version 3 of the License, or (at your option) any later
218+# version.
219+#
220+# This program is distributed in the hope that it will be useful, but WITHOUT
221+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
222+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
223+# details.
224+#
225+# You should have received a copy of the GNU General Public License along with
226+# this program. If not, see <http://www.gnu.org/licenses/>.
227+#
228+
229+import argparse
230+import re
231+import subprocess
232+import sys
233+import traceback
234+import warnings
235+
236+
237+def ishostnamey(name):
238+ """Return true if the passed name is roughly hostnamey. NTP is rather casual about how it
239+ reports hostnames and IP addresses, so we can't be too strict. This function simply tests
240+ that all of the characters in the string are letters, digits, dash, or period."""
241+ return re.search(r'^[\w.-]*$', name) is not None and name.find('_') == -1
242+
243+
244+def isipaddressy(name):
245+ """Return true if the passed name is roughly IP addressy. NTP is rather casual about how it
246+ reports hostnames and IP addresses, so we can't be too strict. This function simply tests
247+ that all of the characters in the string are hexadecimal digits, period, or colon."""
248+ return re.search(r'^[0-9a-f.:]*$', name) is not None
249+
250+
251+class CheckNTPMon(object):
252+
253+ def __init__(self,
254+ warnpeers=2,
255+ okpeers=4,
256+ warnoffset=10,
257+ critoffset=50,
258+ warnreach=75,
259+ critreach=50):
260+
261+ self.warnpeers = warnpeers
262+ self.okpeers = okpeers
263+ self.warnoffset = warnoffset
264+ self.critoffset = critoffset
265+ self.warnreach = warnreach
266+ self.critreach = critreach
267+
268+ def peers(self, n):
269+ """Return 0 if the number of peers is OK
270+ Return 1 if the number of peers is WARNING
271+ Return 2 if the number of peers is CRITICAL"""
272+ if n >= self.okpeers:
273+ print "OK: %d usable peers" % n
274+ return 0
275+ elif n < self.warnpeers:
276+ print "CRITICAL: Too few peers (%d) - must be at least %d" % (n, self.warnpeers)
277+ return 2
278+ else:
279+ print "WARNING: Too few peers (%d) - should be at least %d" % (n, self.okpeers)
280+ return 1
281+
282+ def offset(self, offset):
283+ """Return 0 if the offset is OK
284+ Return 1 if the offset is WARNING
285+ Return 2 if the offset is CRITICAL"""
286+ if abs(offset) > self.critoffset:
287+ print "CRITICAL: Offset too high (%g) - must be less than %g" % \
288+ (offset, self.critoffset)
289+ return 2
290+ if abs(offset) > self.warnoffset:
291+ print "WARNING: Offset too high (%g) - should be less than %g" % \
292+ (offset, self.warnoffset)
293+ return 1
294+ else:
295+ print "OK: Offset normal (%g)" % (offset)
296+ return 0
297+
298+ def reachability(self, percent):
299+ """Return 0 if the reachability percentage is OK
300+ Return 1 if the reachability percentage is warning
301+ Return 2 if the reachability percentage is critical
302+ Raise a ValueError if reachability is not a percentage"""
303+ if percent < 0 or percent > 100:
304+ raise ValueError('Value must be a percentage')
305+ if percent <= self.critreach:
306+ print "CRITICAL: Reachability too low (%g%%) - must be more than %g%%" % \
307+ (percent, self.critreach)
308+ return 2
309+ elif percent <= self.warnreach:
310+ print "WARNING: Reachability too low (%g%%) - should be more than %g%%" % \
311+ (percent, self.warnreach)
312+ return 1
313+ else:
314+ print "OK: Reachability normal (%g%%)" % (percent)
315+ return 0
316+
317+ def sync(self, synchost):
318+ """Return 0 if the synchost is non-zero in length and is a roughly valid host identifier, return 2 otherwise."""
319+ synced = len(synchost) > 0 and (ishostnamey(synchost) or isipaddressy(synchost))
320+ if synced:
321+ print "OK: time is in sync with %s" % (synchost)
322+ else:
323+ print "CRITICAL: no sync host selected"
324+ return 0 if synced else 2
325+
326+
327+class NTPPeers(object):
328+ """Turn the peer lines returned by 'ntpq -pn' into a data structure usable for checks."""
329+
330+ noiselines = [
331+ r'remote\s+refid\s+st\s+t\s+when\s+poll\s+reach\s+',
332+ r'^=*$',
333+ r'No association ID.s returned',
334+ ]
335+ ignorepeers = [".LOCL.", ".INIT.", ".XFAC."]
336+
337+ def isnoiseline(self, line):
338+ for regex in self.noiselines:
339+ if re.search(regex, line) is not None:
340+ return True
341+ return False
342+
343+ def shouldignore(self, fields, l):
344+ if len(fields) != 10:
345+ warnings.warn('Invalid ntpq peer line - there are %d fields: %s' % (len(fields), l))
346+ return True
347+ if fields[1] in self.ignorepeers:
348+ return True
349+ if int(fields[2]) > 15:
350+ return True
351+ return False
352+
353+ def parsetally(self, tally, peerdata, offset):
354+ """Parse the tally code and add the appropriate items to the peer data based on that code.
355+ See the explanation of tally codes in the ntpq documentation for how these work:
356+ - http://www.eecis.udel.edu/~mills/ntp/html/decode.html#peer
357+ - http://www.eecis.udel.edu/~mills/ntp/html/ntpq.html
358+ - http://psp2.ntp.org/bin/view/Support/TroubleshootingNTP
359+ """
360+ if tally in ['*', 'o'] and 'syncpeer' not in self.ntpdata:
361+ # this is our sync peer
362+ self.ntpdata['syncpeer'] = peerdata['peer']
363+ self.ntpdata['offsetsyncpeer'] = offset
364+ self.ntpdata['survivors'] += 1
365+ self.ntpdata['offsetsurvivors'] += offset
366+ elif tally in ['+', '#']:
367+ # valid peer
368+ self.ntpdata['survivors'] += 1
369+ self.ntpdata['offsetsurvivors'] += offset
370+ elif tally in [' ', 'x', '.', '-']:
371+ # discarded peer
372+ self.ntpdata['discards'] += 1
373+ self.ntpdata['offsetdiscards'] += offset
374+ else:
375+ self.ntpdata['unknown'] += 1
376+ return False
377+ return True
378+
379+ def __init__(self, peerlines, check=None):
380+ self.ntpdata = {
381+ 'survivors': 0,
382+ 'offsetsurvivors': 0,
383+ 'discards': 0,
384+ 'offsetdiscards': 0,
385+ 'unknown': 0,
386+ 'peers': 0,
387+ 'offsetall': 0,
388+ 'totalreach': 0,
389+ }
390+ self.check = check
391+
392+ for l in peerlines:
393+ if self.isnoiseline(l):
394+ continue
395+
396+ # first column is the tally field, the rest are whitespace-separated fields
397+ tally = l[0]
398+ fields = l[1:-1].split()
399+
400+ if self.shouldignore(fields, l):
401+ continue
402+
403+ fieldnames = ['peer', 'refid', 'stratum', 'type', 'lastpoll', 'interval', 'reach',
404+ 'delay', 'offset', 'jitter']
405+ peerdata = dict(zip(fieldnames, fields))
406+
407+ offset = abs(float(peerdata['offset']))
408+ if not self.parsetally(tally, peerdata, offset):
409+ warnings.warn('Unknown tally code detected - please report a bug: %s' % (l))
410+ continue
411+
412+ self.ntpdata['peers'] += 1
413+ self.ntpdata['offsetall'] += offset
414+
415+ # reachability - this counts the number of bits set in the reachability field
416+ # (which is displayed in octal in the ntpq output)
417+ # http://stackoverflow.com/questions/9829578/fast-way-of-counting-bits-in-python
418+ self.ntpdata['totalreach'] += bin(int(peerdata['reach'], 8)).count("1")
419+
420+ # reachability as a percentage of the last 8 polls, across all peers
421+ self.ntpdata['reachability'] = float(self.ntpdata['totalreach']) * 100 / self.ntpdata['peers'] / 8
422+
423+ # average offsets
424+ if self.ntpdata['survivors'] > 0:
425+ self.ntpdata['averageoffsetsurvivors'] = \
426+ self.ntpdata['offsetsurvivors'] / self.ntpdata['survivors']
427+ if self.ntpdata['discards'] > 0:
428+ self.ntpdata['averageoffsetdiscards'] = \
429+ self.ntpdata['offsetdiscards'] / self.ntpdata['discards']
430+ self.ntpdata['averageoffset'] = self.ntpdata['offsetall'] / self.ntpdata['peers']
431+
432+ def dump(self):
433+ if self.ntpdata.get('syncpeer'):
434+ print "Synced to: %s, offset %g ms" % \
435+ (self.ntpdata['syncpeer'], self.ntpdata['offsetsyncpeer'])
436+ else:
437+ print "No remote sync peer"
438+ print "%d total peers, average offset %g ms" % \
439+ (self.ntpdata['peers'], self.ntpdata['averageoffset'])
440+ if self.ntpdata['survivors'] > 0:
441+ print "%d good peers, average offset %g ms" % \
442+ (self.ntpdata['survivors'], self.ntpdata['averageoffsetsurvivors'])
443+ if self.ntpdata['discards'] > 0:
444+ print "%d discarded peers, average offset %g ms" % \
445+ (self.ntpdata['discards'], self.ntpdata['averageoffsetdiscards'])
446+ print "Average reachability of all peers: %d%%" % (self.ntpdata['reachability'])
447+
448+ def check_peers(self, check=None):
449+ """Check the number of usable peers"""
450+ if check is None:
451+ check = self.check if self.check else CheckNTPMon()
452+ return check.peers(self.ntpdata['peers'])
453+
454+ def check_offset(self, check=None):
455+ """Check the offset from the sync peer, returning critical, warning,
456+ or OK based on the CheckNTPMon results.
457+ If there is no sync peer, use the average offset of survivors instead.
458+ If there are no survivors, use the average offset of discards instead, and return warning as a minimum.
459+ If there are no discards, return critical.
460+ """
461+ if check is None:
462+ check = self.check if self.check else CheckNTPMon()
463+ if 'offsetsyncpeer' in self.ntpdata:
464+ return check.offset(self.ntpdata['offsetsyncpeer'])
465+ if 'averageoffsetsurvivors' in self.ntpdata:
466+ return check.offset(self.ntpdata['averageoffsetsurvivors'])
467+ if 'averageoffsetdiscards' in self.ntpdata:
468+ result = check.offset(self.ntpdata['averageoffsetdiscards'])
469+ return 1 if result < 1 else result
470+ else:
471+ print "CRITICAL: No peers for which to check offset"
472+ return 2
473+
474+ def check_reachability(self, check=None):
475+ """Check reachability of all peers"""
476+ if check is None:
477+ check = self.check if self.check else CheckNTPMon()
478+ return check.reachability(self.ntpdata['reachability'])
479+
480+ def check_sync(self, check=None):
481+ """Check whether host is in sync with a peer"""
482+ if check is None:
483+ check = self.check if self.check else CheckNTPMon()
484+ if self.ntpdata.get('syncpeer') is None:
485+ print "CRITICAL: No sync peer"
486+ return 2
487+ return check.sync(self.ntpdata['syncpeer'])
488+
489+ def checks(self, methods=None, check=None):
490+ ret = 0
491+ if not methods:
492+ methods = [self.check_offset, self.check_peers, self.check_reachability, self.check_sync]
493+ for method in methods:
494+ check = method()
495+ if ret < check:
496+ ret = check
497+ return ret
498+
499+ @staticmethod
500+ def query():
501+ lines = None
502+ try:
503+ output = subprocess.check_output(["ntpq", "-pn"])
504+ lines = output.split("\n")
505+ except:
506+ traceback.print_exc(file=sys.stdout)
507+ return lines
508+
509+
510+def main():
511+ methodnames = ['offset', 'peers', 'reachability', 'sync']
512+ options = {
513+ 'warnpeers': [ 2, int, 'Minimum number of peers to be considered non-critical'],
514+ 'okpeers': [ 4, int, 'Minimum number of peers to be considered OK'],
515+ 'warnoffset': [ 10, float, 'Minimum offset to be considered warning'],
516+ 'critoffset': [ 50, float, 'Minimum offset to be considered critical'],
517+ 'warnreach': [ 75, float, 'Minimum peer reachability percentage to be considered OK'],
518+ 'critreach': [ 50, float, 'Minimum peer reachability percentage to be considered non-crtical'],
519+ }
520+
521+ # Create check ranges; will be used by parse_args to store options
522+ checkntpmon = CheckNTPMon()
523+
524+ # parse command line
525+ parser = argparse.ArgumentParser(description='Nagios NTP check incorporating the logic of NTPmon')
526+ parser.add_argument('--check', choices=methodnames,
527+ help='Select check to run; if omitted, run all checks and return the worst result.')
528+ parser.add_argument('--debug', action='store_true',
529+ help='Include "ntpq -pn" output and internal state dump along with check results.')
530+ for o in options.keys():
531+ helptext = options[o][2] + ' (default: %d)' % (options[o][0])
532+ parser.add_argument('--' + o, default=options[o][0], help=helptext, type=options[o][1])
533+ args = parser.parse_args(namespace=checkntpmon)
534+
535+ # run ntpq
536+ lines = NTPPeers.query()
537+ if lines is None:
538+ # Unknown result
539+ print "Cannot get peers from ntpq."
540+ print "Please check that an NTP server is installed and functional."
541+ sys.exit(3)
542+
543+ # initialise our object with the results of ntpq and our preferred check thresholds
544+ ntp = NTPPeers(lines, checkntpmon)
545+
546+ if args.debug:
547+ print "\n".join(lines)
548+ ntp.dump()
549+
550+ # work out which method to run
551+ # (methods must be in the same order as methodnames above)
552+ methods = [ntp.check_offset, ntp.check_peers, ntp.check_reachability, ntp.check_sync]
553+ checkmethods = dict(zip(methodnames, methods))
554+
555+ # if check argument is specified, run just that check
556+ ret = 0
557+ if checkmethods.get(args.check):
558+ method = checkmethods[args.check]
559+ ret = method()
560+ # else check all the methods
561+ else:
562+ ret = ntp.checks(methods)
563+
564+ sys.exit(ret)
565+
566+if __name__ == "__main__":
567+ main()
568+
569
570=== modified file 'hooks/charmhelpers/__init__.py'
571--- hooks/charmhelpers/__init__.py 2013-07-09 11:17:24 +0000
572+++ hooks/charmhelpers/__init__.py 2015-03-23 00:51:45 +0000
573@@ -0,0 +1,38 @@
574+# Copyright 2014-2015 Canonical Limited.
575+#
576+# This file is part of charm-helpers.
577+#
578+# charm-helpers is free software: you can redistribute it and/or modify
579+# it under the terms of the GNU Lesser General Public License version 3 as
580+# published by the Free Software Foundation.
581+#
582+# charm-helpers is distributed in the hope that it will be useful,
583+# but WITHOUT ANY WARRANTY; without even the implied warranty of
584+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
585+# GNU Lesser General Public License for more details.
586+#
587+# You should have received a copy of the GNU Lesser General Public License
588+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
589+
590+# Bootstrap charm-helpers, installing its dependencies if necessary using
591+# only standard libraries.
592+import subprocess
593+import sys
594+
595+try:
596+ import six # flake8: noqa
597+except ImportError:
598+ if sys.version_info.major == 2:
599+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
600+ else:
601+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
602+ import six # flake8: noqa
603+
604+try:
605+ import yaml # flake8: noqa
606+except ImportError:
607+ if sys.version_info.major == 2:
608+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
609+ else:
610+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
611+ import yaml # flake8: noqa
612
613=== modified file 'hooks/charmhelpers/contrib/__init__.py'
614--- hooks/charmhelpers/contrib/__init__.py 2014-09-08 16:38:59 +0000
615+++ hooks/charmhelpers/contrib/__init__.py 2015-03-23 00:51:45 +0000
616@@ -0,0 +1,15 @@
617+# Copyright 2014-2015 Canonical Limited.
618+#
619+# This file is part of charm-helpers.
620+#
621+# charm-helpers is free software: you can redistribute it and/or modify
622+# it under the terms of the GNU Lesser General Public License version 3 as
623+# published by the Free Software Foundation.
624+#
625+# charm-helpers is distributed in the hope that it will be useful,
626+# but WITHOUT ANY WARRANTY; without even the implied warranty of
627+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
628+# GNU Lesser General Public License for more details.
629+#
630+# You should have received a copy of the GNU Lesser General Public License
631+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
632
633=== added directory 'hooks/charmhelpers/contrib/charmsupport'
634=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
635--- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
636+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-03-23 00:51:45 +0000
637@@ -0,0 +1,15 @@
638+# Copyright 2014-2015 Canonical Limited.
639+#
640+# This file is part of charm-helpers.
641+#
642+# charm-helpers is free software: you can redistribute it and/or modify
643+# it under the terms of the GNU Lesser General Public License version 3 as
644+# published by the Free Software Foundation.
645+#
646+# charm-helpers is distributed in the hope that it will be useful,
647+# but WITHOUT ANY WARRANTY; without even the implied warranty of
648+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
649+# GNU Lesser General Public License for more details.
650+#
651+# You should have received a copy of the GNU Lesser General Public License
652+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
653
654=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
655--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
656+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-23 00:51:45 +0000
657@@ -0,0 +1,358 @@
658+# Copyright 2014-2015 Canonical Limited.
659+#
660+# This file is part of charm-helpers.
661+#
662+# charm-helpers is free software: you can redistribute it and/or modify
663+# it under the terms of the GNU Lesser General Public License version 3 as
664+# published by the Free Software Foundation.
665+#
666+# charm-helpers is distributed in the hope that it will be useful,
667+# but WITHOUT ANY WARRANTY; without even the implied warranty of
668+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
669+# GNU Lesser General Public License for more details.
670+#
671+# You should have received a copy of the GNU Lesser General Public License
672+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
673+
674+"""Compatibility with the nrpe-external-master charm"""
675+# Copyright 2012 Canonical Ltd.
676+#
677+# Authors:
678+# Matthew Wedgwood <matthew.wedgwood@canonical.com>
679+
680+import subprocess
681+import pwd
682+import grp
683+import os
684+import glob
685+import shutil
686+import re
687+import shlex
688+import yaml
689+
690+from charmhelpers.core.hookenv import (
691+ config,
692+ local_unit,
693+ log,
694+ relation_ids,
695+ relation_set,
696+ relations_of_type,
697+)
698+
699+from charmhelpers.core.host import service
700+
701+# This module adds compatibility with the nrpe-external-master and plain nrpe
702+# subordinate charms. To use it in your charm:
703+#
704+# 1. Update metadata.yaml
705+#
706+# provides:
707+# (...)
708+# nrpe-external-master:
709+# interface: nrpe-external-master
710+# scope: container
711+#
712+# and/or
713+#
714+# provides:
715+# (...)
716+# local-monitors:
717+# interface: local-monitors
718+# scope: container
719+
720+#
721+# 2. Add the following to config.yaml
722+#
723+# nagios_context:
724+# default: "juju"
725+# type: string
726+# description: |
727+# Used by the nrpe subordinate charms.
728+# A string that will be prepended to instance name to set the host name
729+# in nagios. So for instance the hostname would be something like:
730+# juju-myservice-0
731+# If you're running multiple environments with the same services in them
732+# this allows you to differentiate between them.
733+# nagios_servicegroups:
734+# default: ""
735+# type: string
736+# description: |
737+# A comma-separated list of nagios servicegroups.
738+# If left empty, the nagios_context will be used as the servicegroup
739+#
740+# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
741+#
742+# 4. Update your hooks.py with something like this:
743+#
744+# from charmsupport.nrpe import NRPE
745+# (...)
746+# def update_nrpe_config():
747+# nrpe_compat = NRPE()
748+# nrpe_compat.add_check(
749+# shortname = "myservice",
750+# description = "Check MyService",
751+# check_cmd = "check_http -w 2 -c 10 http://localhost"
752+# )
753+# nrpe_compat.add_check(
754+# "myservice_other",
755+# "Check for widget failures",
756+# check_cmd = "/srv/myapp/scripts/widget_check"
757+# )
758+# nrpe_compat.write()
759+#
760+# def config_changed():
761+# (...)
762+# update_nrpe_config()
763+#
764+# def nrpe_external_master_relation_changed():
765+# update_nrpe_config()
766+#
767+# def local_monitors_relation_changed():
768+# update_nrpe_config()
769+#
770+# 5. ln -s hooks.py nrpe-external-master-relation-changed
771+# ln -s hooks.py local-monitors-relation-changed
772+
773+
774+class CheckException(Exception):
775+ pass
776+
777+
778+class Check(object):
779+ shortname_re = '[A-Za-z0-9-_]+$'
780+ service_template = ("""
781+#---------------------------------------------------
782+# This file is Juju managed
783+#---------------------------------------------------
784+define service {{
785+ use active-service
786+ host_name {nagios_hostname}
787+ service_description {nagios_hostname}[{shortname}] """
788+ """{description}
789+ check_command check_nrpe!{command}
790+ servicegroups {nagios_servicegroup}
791+}}
792+""")
793+
794+ def __init__(self, shortname, description, check_cmd):
795+ super(Check, self).__init__()
796+ # XXX: could be better to calculate this from the service name
797+ if not re.match(self.shortname_re, shortname):
798+ raise CheckException("shortname must match {}".format(
799+ Check.shortname_re))
800+ self.shortname = shortname
801+ self.command = "check_{}".format(shortname)
802+ # Note: a set of invalid characters is defined by the
803+ # Nagios server config
804+ # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
805+ self.description = description
806+ self.check_cmd = self._locate_cmd(check_cmd)
807+
808+ def _locate_cmd(self, check_cmd):
809+ search_path = (
810+ '/usr/lib/nagios/plugins',
811+ '/usr/local/lib/nagios/plugins',
812+ )
813+ parts = shlex.split(check_cmd)
814+ for path in search_path:
815+ if os.path.exists(os.path.join(path, parts[0])):
816+ command = os.path.join(path, parts[0])
817+ if len(parts) > 1:
818+ command += " " + " ".join(parts[1:])
819+ return command
820+ log('Check command not found: {}'.format(parts[0]))
821+ return ''
822+
823+ def write(self, nagios_context, hostname, nagios_servicegroups):
824+ nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
825+ self.command)
826+ with open(nrpe_check_file, 'w') as nrpe_check_config:
827+ nrpe_check_config.write("# check {}\n".format(self.shortname))
828+ nrpe_check_config.write("command[{}]={}\n".format(
829+ self.command, self.check_cmd))
830+
831+ if not os.path.exists(NRPE.nagios_exportdir):
832+ log('Not writing service config as {} is not accessible'.format(
833+ NRPE.nagios_exportdir))
834+ else:
835+ self.write_service_config(nagios_context, hostname,
836+ nagios_servicegroups)
837+
838+ def write_service_config(self, nagios_context, hostname,
839+ nagios_servicegroups):
840+ for f in os.listdir(NRPE.nagios_exportdir):
841+ if re.search('.*{}.cfg'.format(self.command), f):
842+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
843+
844+ templ_vars = {
845+ 'nagios_hostname': hostname,
846+ 'nagios_servicegroup': nagios_servicegroups,
847+ 'description': self.description,
848+ 'shortname': self.shortname,
849+ 'command': self.command,
850+ }
851+ nrpe_service_text = Check.service_template.format(**templ_vars)
852+ nrpe_service_file = '{}/service__{}_{}.cfg'.format(
853+ NRPE.nagios_exportdir, hostname, self.command)
854+ with open(nrpe_service_file, 'w') as nrpe_service_config:
855+ nrpe_service_config.write(str(nrpe_service_text))
856+
857+ def run(self):
858+ subprocess.call(self.check_cmd)
859+
860+
861+class NRPE(object):
862+ nagios_logdir = '/var/log/nagios'
863+ nagios_exportdir = '/var/lib/nagios/export'
864+ nrpe_confdir = '/etc/nagios/nrpe.d'
865+
866+ def __init__(self, hostname=None):
867+ super(NRPE, self).__init__()
868+ self.config = config()
869+ self.nagios_context = self.config['nagios_context']
870+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
871+ self.nagios_servicegroups = self.config['nagios_servicegroups']
872+ else:
873+ self.nagios_servicegroups = self.nagios_context
874+ self.unit_name = local_unit().replace('/', '-')
875+ if hostname:
876+ self.hostname = hostname
877+ else:
878+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
879+ self.checks = []
880+
881+ def add_check(self, *args, **kwargs):
882+ self.checks.append(Check(*args, **kwargs))
883+
884+ def write(self):
885+ try:
886+ nagios_uid = pwd.getpwnam('nagios').pw_uid
887+ nagios_gid = grp.getgrnam('nagios').gr_gid
888+ except:
889+ log("Nagios user not set up, nrpe checks not updated")
890+ return
891+
892+ if not os.path.exists(NRPE.nagios_logdir):
893+ os.mkdir(NRPE.nagios_logdir)
894+ os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
895+
896+ nrpe_monitors = {}
897+ monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
898+ for nrpecheck in self.checks:
899+ nrpecheck.write(self.nagios_context, self.hostname,
900+ self.nagios_servicegroups)
901+ nrpe_monitors[nrpecheck.shortname] = {
902+ "command": nrpecheck.command,
903+ }
904+
905+ service('restart', 'nagios-nrpe-server')
906+
907+ for rid in relation_ids("local-monitors"):
908+ relation_set(relation_id=rid, monitors=yaml.dump(monitors))
909+
910+
911+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
912+ """
913+ Query relation with nrpe subordinate, return the nagios_host_context
914+
915+ :param str relation_name: Name of relation nrpe sub joined to
916+ """
917+ for rel in relations_of_type(relation_name):
918+ if 'nagios_hostname' in rel:
919+ return rel['nagios_host_context']
920+
921+
922+def get_nagios_hostname(relation_name='nrpe-external-master'):
923+ """
924+ Query relation with nrpe subordinate, return the nagios_hostname
925+
926+ :param str relation_name: Name of relation nrpe sub joined to
927+ """
928+ for rel in relations_of_type(relation_name):
929+ if 'nagios_hostname' in rel:
930+ return rel['nagios_hostname']
931+
932+
933+def get_nagios_unit_name(relation_name='nrpe-external-master'):
934+ """
935+ Return the nagios unit name prepended with host_context if needed
936+
937+ :param str relation_name: Name of relation nrpe sub joined to
938+ """
939+ host_context = get_nagios_hostcontext(relation_name)
940+ if host_context:
941+ unit = "%s:%s" % (host_context, local_unit())
942+ else:
943+ unit = local_unit()
944+ return unit
945+
946+
947+def add_init_service_checks(nrpe, services, unit_name):
948+ """
949+ Add checks for each service in list
950+
951+ :param NRPE nrpe: NRPE object to add check to
952+ :param list services: List of services to check
953+ :param str unit_name: Unit name to use in check description
954+ """
955+ for svc in services:
956+ upstart_init = '/etc/init/%s.conf' % svc
957+ sysv_init = '/etc/init.d/%s' % svc
958+ if os.path.exists(upstart_init):
959+ nrpe.add_check(
960+ shortname=svc,
961+ description='process check {%s}' % unit_name,
962+ check_cmd='check_upstart_job %s' % svc
963+ )
964+ elif os.path.exists(sysv_init):
965+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
966+ cron_file = ('*/5 * * * * root '
967+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
968+ '-s /etc/init.d/%s status > '
969+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
970+ svc)
971+ )
972+ f = open(cronpath, 'w')
973+ f.write(cron_file)
974+ f.close()
975+ nrpe.add_check(
976+ shortname=svc,
977+ description='process check {%s}' % unit_name,
978+ check_cmd='check_status_file.py -f '
979+ '/var/lib/nagios/service-check-%s.txt' % svc,
980+ )
981+
982+
983+def copy_nrpe_checks():
984+ """
985+ Copy the nrpe checks into place
986+
987+ """
988+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
989+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
990+ 'charmhelpers', 'contrib', 'openstack',
991+ 'files')
992+
993+ if not os.path.exists(NAGIOS_PLUGINS):
994+ os.makedirs(NAGIOS_PLUGINS)
995+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
996+ if os.path.isfile(fname):
997+ shutil.copy2(fname,
998+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
999+
1000+
1001+def add_haproxy_checks(nrpe, unit_name):
1002+ """
1003+ Add checks for each service in list
1004+
1005+ :param NRPE nrpe: NRPE object to add check to
1006+ :param str unit_name: Unit name to use in check description
1007+ """
1008+ nrpe.add_check(
1009+ shortname='haproxy_servers',
1010+ description='Check HAProxy {%s}' % unit_name,
1011+ check_cmd='check_haproxy.sh')
1012+ nrpe.add_check(
1013+ shortname='haproxy_queue',
1014+ description='Check HAProxy queue depth {%s}' % unit_name,
1015+ check_cmd='check_haproxy_queue_depth.sh')
1016
1017=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
1018--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
1019+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-03-23 00:51:45 +0000
1020@@ -0,0 +1,175 @@
1021+# Copyright 2014-2015 Canonical Limited.
1022+#
1023+# This file is part of charm-helpers.
1024+#
1025+# charm-helpers is free software: you can redistribute it and/or modify
1026+# it under the terms of the GNU Lesser General Public License version 3 as
1027+# published by the Free Software Foundation.
1028+#
1029+# charm-helpers is distributed in the hope that it will be useful,
1030+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1031+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1032+# GNU Lesser General Public License for more details.
1033+#
1034+# You should have received a copy of the GNU Lesser General Public License
1035+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1036+
1037+'''
1038+Functions for managing volumes in juju units. One volume is supported per unit.
1039+Subordinates may have their own storage, provided it is on its own partition.
1040+
1041+Configuration stanzas::
1042+
1043+ volume-ephemeral:
1044+ type: boolean
1045+ default: true
1046+ description: >
1047+ If false, a volume is mounted as sepecified in "volume-map"
1048+ If true, ephemeral storage will be used, meaning that log data
1049+ will only exist as long as the machine. YOU HAVE BEEN WARNED.
1050+ volume-map:
1051+ type: string
1052+ default: {}
1053+ description: >
1054+ YAML map of units to device names, e.g:
1055+ "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
1056+ Service units will raise a configure-error if volume-ephemeral
1057+ is 'true' and no volume-map value is set. Use 'juju set' to set a
1058+ value and 'juju resolved' to complete configuration.
1059+
1060+Usage::
1061+
1062+ from charmsupport.volumes import configure_volume, VolumeConfigurationError
1063+ from charmsupport.hookenv import log, ERROR
1064+ def post_mount_hook():
1065+ stop_service('myservice')
1066+ def post_mount_hook():
1067+ start_service('myservice')
1068+
1069+ if __name__ == '__main__':
1070+ try:
1071+ configure_volume(before_change=pre_mount_hook,
1072+ after_change=post_mount_hook)
1073+ except VolumeConfigurationError:
1074+ log('Storage could not be configured', ERROR)
1075+
1076+'''
1077+
1078+# XXX: Known limitations
1079+# - fstab is neither consulted nor updated
1080+
1081+import os
1082+from charmhelpers.core import hookenv
1083+from charmhelpers.core import host
1084+import yaml
1085+
1086+
1087+MOUNT_BASE = '/srv/juju/volumes'
1088+
1089+
1090+class VolumeConfigurationError(Exception):
1091+ '''Volume configuration data is missing or invalid'''
1092+ pass
1093+
1094+
1095+def get_config():
1096+ '''Gather and sanity-check volume configuration data'''
1097+ volume_config = {}
1098+ config = hookenv.config()
1099+
1100+ errors = False
1101+
1102+ if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
1103+ volume_config['ephemeral'] = True
1104+ else:
1105+ volume_config['ephemeral'] = False
1106+
1107+ try:
1108+ volume_map = yaml.safe_load(config.get('volume-map', '{}'))
1109+ except yaml.YAMLError as e:
1110+ hookenv.log("Error parsing YAML volume-map: {}".format(e),
1111+ hookenv.ERROR)
1112+ errors = True
1113+ if volume_map is None:
1114+ # probably an empty string
1115+ volume_map = {}
1116+ elif not isinstance(volume_map, dict):
1117+ hookenv.log("Volume-map should be a dictionary, not {}".format(
1118+ type(volume_map)))
1119+ errors = True
1120+
1121+ volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
1122+ if volume_config['device'] and volume_config['ephemeral']:
1123+ # asked for ephemeral storage but also defined a volume ID
1124+ hookenv.log('A volume is defined for this unit, but ephemeral '
1125+ 'storage was requested', hookenv.ERROR)
1126+ errors = True
1127+ elif not volume_config['device'] and not volume_config['ephemeral']:
1128+ # asked for permanent storage but did not define volume ID
1129+ hookenv.log('Ephemeral storage was requested, but there is no volume '
1130+ 'defined for this unit.', hookenv.ERROR)
1131+ errors = True
1132+
1133+ unit_mount_name = hookenv.local_unit().replace('/', '-')
1134+ volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
1135+
1136+ if errors:
1137+ return None
1138+ return volume_config
1139+
1140+
1141+def mount_volume(config):
1142+ if os.path.exists(config['mountpoint']):
1143+ if not os.path.isdir(config['mountpoint']):
1144+ hookenv.log('Not a directory: {}'.format(config['mountpoint']))
1145+ raise VolumeConfigurationError()
1146+ else:
1147+ host.mkdir(config['mountpoint'])
1148+ if os.path.ismount(config['mountpoint']):
1149+ unmount_volume(config)
1150+ if not host.mount(config['device'], config['mountpoint'], persist=True):
1151+ raise VolumeConfigurationError()
1152+
1153+
1154+def unmount_volume(config):
1155+ if os.path.ismount(config['mountpoint']):
1156+ if not host.umount(config['mountpoint'], persist=True):
1157+ raise VolumeConfigurationError()
1158+
1159+
1160+def managed_mounts():
1161+ '''List of all mounted managed volumes'''
1162+ return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
1163+
1164+
1165+def configure_volume(before_change=lambda: None, after_change=lambda: None):
1166+ '''Set up storage (or don't) according to the charm's volume configuration.
1167+ Returns the mount point or "ephemeral". before_change and after_change
1168+ are optional functions to be called if the volume configuration changes.
1169+ '''
1170+
1171+ config = get_config()
1172+ if not config:
1173+ hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
1174+ raise VolumeConfigurationError()
1175+
1176+ if config['ephemeral']:
1177+ if os.path.ismount(config['mountpoint']):
1178+ before_change()
1179+ unmount_volume(config)
1180+ after_change()
1181+ return 'ephemeral'
1182+ else:
1183+ # persistent storage
1184+ if os.path.ismount(config['mountpoint']):
1185+ mounts = dict(managed_mounts())
1186+ if mounts.get(config['mountpoint']) != config['device']:
1187+ before_change()
1188+ unmount_volume(config)
1189+ mount_volume(config)
1190+ after_change()
1191+ else:
1192+ before_change()
1193+ mount_volume(config)
1194+ after_change()
1195+ return config['mountpoint']
1196
1197=== modified file 'hooks/charmhelpers/contrib/templating/__init__.py'
1198--- hooks/charmhelpers/contrib/templating/__init__.py 2014-09-08 16:38:59 +0000
1199+++ hooks/charmhelpers/contrib/templating/__init__.py 2015-03-23 00:51:45 +0000
1200@@ -0,0 +1,15 @@
1201+# Copyright 2014-2015 Canonical Limited.
1202+#
1203+# This file is part of charm-helpers.
1204+#
1205+# charm-helpers is free software: you can redistribute it and/or modify
1206+# it under the terms of the GNU Lesser General Public License version 3 as
1207+# published by the Free Software Foundation.
1208+#
1209+# charm-helpers is distributed in the hope that it will be useful,
1210+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1211+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1212+# GNU Lesser General Public License for more details.
1213+#
1214+# You should have received a copy of the GNU Lesser General Public License
1215+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1216
1217=== modified file 'hooks/charmhelpers/contrib/templating/jinja.py'
1218--- hooks/charmhelpers/contrib/templating/jinja.py 2014-09-08 16:38:59 +0000
1219+++ hooks/charmhelpers/contrib/templating/jinja.py 2015-03-23 00:51:45 +0000
1220@@ -1,21 +1,37 @@
1221+# Copyright 2014-2015 Canonical Limited.
1222+#
1223+# This file is part of charm-helpers.
1224+#
1225+# charm-helpers is free software: you can redistribute it and/or modify
1226+# it under the terms of the GNU Lesser General Public License version 3 as
1227+# published by the Free Software Foundation.
1228+#
1229+# charm-helpers is distributed in the hope that it will be useful,
1230+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1231+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1232+# GNU Lesser General Public License for more details.
1233+#
1234+# You should have received a copy of the GNU Lesser General Public License
1235+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1236+
1237 """
1238 Templating using the python-jinja2 package.
1239 """
1240-from charmhelpers.fetch import (
1241- apt_install,
1242-)
1243-
1244-
1245-DEFAULT_TEMPLATES_DIR = 'templates'
1246-
1247-
1248+import six
1249+from charmhelpers.fetch import apt_install
1250 try:
1251 import jinja2
1252 except ImportError:
1253- apt_install(["python-jinja2"])
1254+ if six.PY3:
1255+ apt_install(["python3-jinja2"])
1256+ else:
1257+ apt_install(["python-jinja2"])
1258 import jinja2
1259
1260
1261+DEFAULT_TEMPLATES_DIR = 'templates'
1262+
1263+
1264 def render(template_name, context, template_dir=DEFAULT_TEMPLATES_DIR):
1265 templates = jinja2.Environment(
1266 loader=jinja2.FileSystemLoader(template_dir))
1267
1268=== modified file 'hooks/charmhelpers/core/__init__.py'
1269--- hooks/charmhelpers/core/__init__.py 2013-07-09 11:17:24 +0000
1270+++ hooks/charmhelpers/core/__init__.py 2015-03-23 00:51:45 +0000
1271@@ -0,0 +1,15 @@
1272+# Copyright 2014-2015 Canonical Limited.
1273+#
1274+# This file is part of charm-helpers.
1275+#
1276+# charm-helpers is free software: you can redistribute it and/or modify
1277+# it under the terms of the GNU Lesser General Public License version 3 as
1278+# published by the Free Software Foundation.
1279+#
1280+# charm-helpers is distributed in the hope that it will be useful,
1281+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1282+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1283+# GNU Lesser General Public License for more details.
1284+#
1285+# You should have received a copy of the GNU Lesser General Public License
1286+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1287
1288=== added file 'hooks/charmhelpers/core/decorators.py'
1289--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
1290+++ hooks/charmhelpers/core/decorators.py 2015-03-23 00:51:45 +0000
1291@@ -0,0 +1,57 @@
1292+# Copyright 2014-2015 Canonical Limited.
1293+#
1294+# This file is part of charm-helpers.
1295+#
1296+# charm-helpers is free software: you can redistribute it and/or modify
1297+# it under the terms of the GNU Lesser General Public License version 3 as
1298+# published by the Free Software Foundation.
1299+#
1300+# charm-helpers is distributed in the hope that it will be useful,
1301+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1302+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1303+# GNU Lesser General Public License for more details.
1304+#
1305+# You should have received a copy of the GNU Lesser General Public License
1306+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1307+
1308+#
1309+# Copyright 2014 Canonical Ltd.
1310+#
1311+# Authors:
1312+# Edward Hope-Morley <opentastic@gmail.com>
1313+#
1314+
1315+import time
1316+
1317+from charmhelpers.core.hookenv import (
1318+ log,
1319+ INFO,
1320+)
1321+
1322+
1323+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
1324+ """If the decorated function raises exception exc_type, allow num_retries
1325+ retry attempts before raise the exception.
1326+ """
1327+ def _retry_on_exception_inner_1(f):
1328+ def _retry_on_exception_inner_2(*args, **kwargs):
1329+ retries = num_retries
1330+ multiplier = 1
1331+ while True:
1332+ try:
1333+ return f(*args, **kwargs)
1334+ except exc_type:
1335+ if not retries:
1336+ raise
1337+
1338+ delay = base_delay * multiplier
1339+ multiplier += 1
1340+ log("Retrying '%s' %d more times (delay=%s)" %
1341+ (f.__name__, retries, delay), level=INFO)
1342+ retries -= 1
1343+ if delay:
1344+ time.sleep(delay)
1345+
1346+ return _retry_on_exception_inner_2
1347+
1348+ return _retry_on_exception_inner_1
1349
1350=== modified file 'hooks/charmhelpers/core/fstab.py'
1351--- hooks/charmhelpers/core/fstab.py 2014-07-16 05:40:55 +0000
1352+++ hooks/charmhelpers/core/fstab.py 2015-03-23 00:51:45 +0000
1353@@ -1,12 +1,29 @@
1354 #!/usr/bin/env python
1355 # -*- coding: utf-8 -*-
1356
1357+# Copyright 2014-2015 Canonical Limited.
1358+#
1359+# This file is part of charm-helpers.
1360+#
1361+# charm-helpers is free software: you can redistribute it and/or modify
1362+# it under the terms of the GNU Lesser General Public License version 3 as
1363+# published by the Free Software Foundation.
1364+#
1365+# charm-helpers is distributed in the hope that it will be useful,
1366+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1367+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1368+# GNU Lesser General Public License for more details.
1369+#
1370+# You should have received a copy of the GNU Lesser General Public License
1371+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1372+
1373+import io
1374+import os
1375+
1376 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1377
1378-import os
1379-
1380-
1381-class Fstab(file):
1382+
1383+class Fstab(io.FileIO):
1384 """This class extends file in order to implement a file reader/writer
1385 for file `/etc/fstab`
1386 """
1387@@ -24,8 +41,8 @@
1388 options = "defaults"
1389
1390 self.options = options
1391- self.d = d
1392- self.p = p
1393+ self.d = int(d)
1394+ self.p = int(p)
1395
1396 def __eq__(self, o):
1397 return str(self) == str(o)
1398@@ -45,7 +62,7 @@
1399 self._path = path
1400 else:
1401 self._path = self.DEFAULT_PATH
1402- file.__init__(self, self._path, 'r+')
1403+ super(Fstab, self).__init__(self._path, 'rb+')
1404
1405 def _hydrate_entry(self, line):
1406 # NOTE: use split with no arguments to split on any
1407@@ -58,8 +75,9 @@
1408 def entries(self):
1409 self.seek(0)
1410 for line in self.readlines():
1411+ line = line.decode('us-ascii')
1412 try:
1413- if not line.startswith("#"):
1414+ if line.strip() and not line.strip().startswith("#"):
1415 yield self._hydrate_entry(line)
1416 except ValueError:
1417 pass
1418@@ -75,18 +93,18 @@
1419 if self.get_entry_by_attr('device', entry.device):
1420 return False
1421
1422- self.write(str(entry) + '\n')
1423+ self.write((str(entry) + '\n').encode('us-ascii'))
1424 self.truncate()
1425 return entry
1426
1427 def remove_entry(self, entry):
1428 self.seek(0)
1429
1430- lines = self.readlines()
1431+ lines = [l.decode('us-ascii') for l in self.readlines()]
1432
1433 found = False
1434 for index, line in enumerate(lines):
1435- if not line.startswith("#"):
1436+ if line.strip() and not line.strip().startswith("#"):
1437 if self._hydrate_entry(line) == entry:
1438 found = True
1439 break
1440@@ -97,7 +115,7 @@
1441 lines.remove(line)
1442
1443 self.seek(0)
1444- self.write(''.join(lines))
1445+ self.write(''.join(lines).encode('us-ascii'))
1446 self.truncate()
1447 return True
1448
1449
1450=== modified file 'hooks/charmhelpers/core/hookenv.py'
1451--- hooks/charmhelpers/core/hookenv.py 2014-09-08 16:38:59 +0000
1452+++ hooks/charmhelpers/core/hookenv.py 2015-03-23 00:51:45 +0000
1453@@ -1,3 +1,19 @@
1454+# Copyright 2014-2015 Canonical Limited.
1455+#
1456+# This file is part of charm-helpers.
1457+#
1458+# charm-helpers is free software: you can redistribute it and/or modify
1459+# it under the terms of the GNU Lesser General Public License version 3 as
1460+# published by the Free Software Foundation.
1461+#
1462+# charm-helpers is distributed in the hope that it will be useful,
1463+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1464+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1465+# GNU Lesser General Public License for more details.
1466+#
1467+# You should have received a copy of the GNU Lesser General Public License
1468+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1469+
1470 "Interactions with the Juju environment"
1471 # Copyright 2013 Canonical Ltd.
1472 #
1473@@ -9,9 +25,14 @@
1474 import yaml
1475 import subprocess
1476 import sys
1477-import UserDict
1478 from subprocess import CalledProcessError
1479
1480+import six
1481+if not six.PY3:
1482+ from UserDict import UserDict
1483+else:
1484+ from collections import UserDict
1485+
1486 CRITICAL = "CRITICAL"
1487 ERROR = "ERROR"
1488 WARNING = "WARNING"
1489@@ -63,16 +84,18 @@
1490 command = ['juju-log']
1491 if level:
1492 command += ['-l', level]
1493+ if not isinstance(message, six.string_types):
1494+ message = repr(message)
1495 command += [message]
1496 subprocess.call(command)
1497
1498
1499-class Serializable(UserDict.IterableUserDict):
1500+class Serializable(UserDict):
1501 """Wrapper, an object that can be serialized to yaml or json"""
1502
1503 def __init__(self, obj):
1504 # wrap the object
1505- UserDict.IterableUserDict.__init__(self)
1506+ UserDict.__init__(self)
1507 self.data = obj
1508
1509 def __getattr__(self, attr):
1510@@ -203,6 +226,23 @@
1511 if os.path.exists(self.path):
1512 self.load_previous()
1513
1514+ def __getitem__(self, key):
1515+ """For regular dict lookups, check the current juju config first,
1516+ then the previous (saved) copy. This ensures that user-saved values
1517+ will be returned by a dict lookup.
1518+
1519+ """
1520+ try:
1521+ return dict.__getitem__(self, key)
1522+ except KeyError:
1523+ return (self._prev_dict or {})[key]
1524+
1525+ def keys(self):
1526+ prev_keys = []
1527+ if self._prev_dict is not None:
1528+ prev_keys = self._prev_dict.keys()
1529+ return list(set(prev_keys + list(dict.keys(self))))
1530+
1531 def load_previous(self, path=None):
1532 """Load previous copy of config from disk.
1533
1534@@ -252,7 +292,7 @@
1535
1536 """
1537 if self._prev_dict:
1538- for k, v in self._prev_dict.iteritems():
1539+ for k, v in six.iteritems(self._prev_dict):
1540 if k not in self:
1541 self[k] = v
1542 with open(self.path, 'w') as f:
1543@@ -267,7 +307,8 @@
1544 config_cmd_line.append(scope)
1545 config_cmd_line.append('--format=json')
1546 try:
1547- config_data = json.loads(subprocess.check_output(config_cmd_line))
1548+ config_data = json.loads(
1549+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
1550 if scope is not None:
1551 return config_data
1552 return Config(config_data)
1553@@ -286,10 +327,10 @@
1554 if unit:
1555 _args.append(unit)
1556 try:
1557- return json.loads(subprocess.check_output(_args))
1558+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1559 except ValueError:
1560 return None
1561- except CalledProcessError, e:
1562+ except CalledProcessError as e:
1563 if e.returncode == 2:
1564 return None
1565 raise
1566@@ -301,7 +342,7 @@
1567 relation_cmd_line = ['relation-set']
1568 if relation_id is not None:
1569 relation_cmd_line.extend(('-r', relation_id))
1570- for k, v in (relation_settings.items() + kwargs.items()):
1571+ for k, v in (list(relation_settings.items()) + list(kwargs.items())):
1572 if v is None:
1573 relation_cmd_line.append('{}='.format(k))
1574 else:
1575@@ -318,7 +359,8 @@
1576 relid_cmd_line = ['relation-ids', '--format=json']
1577 if reltype is not None:
1578 relid_cmd_line.append(reltype)
1579- return json.loads(subprocess.check_output(relid_cmd_line)) or []
1580+ return json.loads(
1581+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
1582 return []
1583
1584
1585@@ -329,7 +371,8 @@
1586 units_cmd_line = ['relation-list', '--format=json']
1587 if relid is not None:
1588 units_cmd_line.extend(('-r', relid))
1589- return json.loads(subprocess.check_output(units_cmd_line)) or []
1590+ return json.loads(
1591+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
1592
1593
1594 @cached
1595@@ -369,21 +412,31 @@
1596
1597
1598 @cached
1599+def metadata():
1600+ """Get the current charm metadata.yaml contents as a python object"""
1601+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
1602+ return yaml.safe_load(md)
1603+
1604+
1605+@cached
1606 def relation_types():
1607 """Get a list of relation types supported by this charm"""
1608- charmdir = os.environ.get('CHARM_DIR', '')
1609- mdf = open(os.path.join(charmdir, 'metadata.yaml'))
1610- md = yaml.safe_load(mdf)
1611 rel_types = []
1612+ md = metadata()
1613 for key in ('provides', 'requires', 'peers'):
1614 section = md.get(key)
1615 if section:
1616 rel_types.extend(section.keys())
1617- mdf.close()
1618 return rel_types
1619
1620
1621 @cached
1622+def charm_name():
1623+ """Get the name of the current charm as is specified on metadata.yaml"""
1624+ return metadata().get('name')
1625+
1626+
1627+@cached
1628 def relations():
1629 """Get a nested dictionary of relation data for all related units"""
1630 rels = {}
1631@@ -438,7 +491,7 @@
1632 """Get the unit ID for the remote unit"""
1633 _args = ['unit-get', '--format=json', attribute]
1634 try:
1635- return json.loads(subprocess.check_output(_args))
1636+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1637 except ValueError:
1638 return None
1639
1640@@ -475,9 +528,10 @@
1641 hooks.execute(sys.argv)
1642 """
1643
1644- def __init__(self):
1645+ def __init__(self, config_save=True):
1646 super(Hooks, self).__init__()
1647 self._hooks = {}
1648+ self._config_save = config_save
1649
1650 def register(self, name, function):
1651 """Register a hook"""
1652@@ -488,9 +542,10 @@
1653 hook_name = os.path.basename(args[0])
1654 if hook_name in self._hooks:
1655 self._hooks[hook_name]()
1656- cfg = config()
1657- if cfg.implicit_save:
1658- cfg.save()
1659+ if self._config_save:
1660+ cfg = config()
1661+ if cfg.implicit_save:
1662+ cfg.save()
1663 else:
1664 raise UnregisteredHookError(hook_name)
1665
1666@@ -511,3 +566,29 @@
1667 def charm_dir():
1668 """Return the root directory of the current charm"""
1669 return os.environ.get('CHARM_DIR')
1670+
1671+
1672+@cached
1673+def action_get(key=None):
1674+ """Gets the value of an action parameter, or all key/value param pairs"""
1675+ cmd = ['action-get']
1676+ if key is not None:
1677+ cmd.append(key)
1678+ cmd.append('--format=json')
1679+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1680+ return action_data
1681+
1682+
1683+def action_set(values):
1684+ """Sets the values to be returned after the action finishes"""
1685+ cmd = ['action-set']
1686+ for k, v in list(values.items()):
1687+ cmd.append('{}={}'.format(k, v))
1688+ subprocess.check_call(cmd)
1689+
1690+
1691+def action_fail(message):
1692+ """Sets the action status to failed and sets the error message.
1693+
1694+ The results set by action_set are preserved."""
1695+ subprocess.check_call(['action-fail', message])
1696
1697=== modified file 'hooks/charmhelpers/core/host.py'
1698--- hooks/charmhelpers/core/host.py 2014-09-08 16:38:59 +0000
1699+++ hooks/charmhelpers/core/host.py 2015-03-23 00:51:45 +0000
1700@@ -1,3 +1,19 @@
1701+# Copyright 2014-2015 Canonical Limited.
1702+#
1703+# This file is part of charm-helpers.
1704+#
1705+# charm-helpers is free software: you can redistribute it and/or modify
1706+# it under the terms of the GNU Lesser General Public License version 3 as
1707+# published by the Free Software Foundation.
1708+#
1709+# charm-helpers is distributed in the hope that it will be useful,
1710+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1711+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1712+# GNU Lesser General Public License for more details.
1713+#
1714+# You should have received a copy of the GNU Lesser General Public License
1715+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1716+
1717 """Tools for working with the host system"""
1718 # Copyright 2012 Canonical Ltd.
1719 #
1720@@ -6,19 +22,20 @@
1721 # Matthew Wedgwood <matthew.wedgwood@canonical.com>
1722
1723 import os
1724+import re
1725 import pwd
1726 import grp
1727 import random
1728 import string
1729 import subprocess
1730 import hashlib
1731-import shutil
1732 from contextlib import contextmanager
1733-
1734 from collections import OrderedDict
1735
1736-from hookenv import log
1737-from fstab import Fstab
1738+import six
1739+
1740+from .hookenv import log
1741+from .fstab import Fstab
1742
1743
1744 def service_start(service_name):
1745@@ -54,7 +71,9 @@
1746 def service_running(service):
1747 """Determine whether a system service is running"""
1748 try:
1749- output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
1750+ output = subprocess.check_output(
1751+ ['service', service, 'status'],
1752+ stderr=subprocess.STDOUT).decode('UTF-8')
1753 except subprocess.CalledProcessError:
1754 return False
1755 else:
1756@@ -67,9 +86,11 @@
1757 def service_available(service_name):
1758 """Determine whether a system service is available"""
1759 try:
1760- subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
1761- except subprocess.CalledProcessError:
1762- return False
1763+ subprocess.check_output(
1764+ ['service', service_name, 'status'],
1765+ stderr=subprocess.STDOUT).decode('UTF-8')
1766+ except subprocess.CalledProcessError as e:
1767+ return 'unrecognized service' not in e.output
1768 else:
1769 return True
1770
1771@@ -96,6 +117,26 @@
1772 return user_info
1773
1774
1775+def add_group(group_name, system_group=False):
1776+ """Add a group to the system"""
1777+ try:
1778+ group_info = grp.getgrnam(group_name)
1779+ log('group {0} already exists!'.format(group_name))
1780+ except KeyError:
1781+ log('creating group {0}'.format(group_name))
1782+ cmd = ['addgroup']
1783+ if system_group:
1784+ cmd.append('--system')
1785+ else:
1786+ cmd.extend([
1787+ '--group',
1788+ ])
1789+ cmd.append(group_name)
1790+ subprocess.check_call(cmd)
1791+ group_info = grp.getgrnam(group_name)
1792+ return group_info
1793+
1794+
1795 def add_user_to_group(username, group):
1796 """Add a user to a group"""
1797 cmd = [
1798@@ -115,7 +156,7 @@
1799 cmd.append(from_path)
1800 cmd.append(to_path)
1801 log(" ".join(cmd))
1802- return subprocess.check_output(cmd).strip()
1803+ return subprocess.check_output(cmd).decode('UTF-8').strip()
1804
1805
1806 def symlink(source, destination):
1807@@ -130,28 +171,31 @@
1808 subprocess.check_call(cmd)
1809
1810
1811-def mkdir(path, owner='root', group='root', perms=0555, force=False):
1812+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
1813 """Create a directory"""
1814 log("Making dir {} {}:{} {:o}".format(path, owner, group,
1815 perms))
1816 uid = pwd.getpwnam(owner).pw_uid
1817 gid = grp.getgrnam(group).gr_gid
1818 realpath = os.path.abspath(path)
1819- if os.path.exists(realpath):
1820- if force and not os.path.isdir(realpath):
1821+ path_exists = os.path.exists(realpath)
1822+ if path_exists and force:
1823+ if not os.path.isdir(realpath):
1824 log("Removing non-directory file {} prior to mkdir()".format(path))
1825 os.unlink(realpath)
1826- else:
1827+ os.makedirs(realpath, perms)
1828+ elif not path_exists:
1829 os.makedirs(realpath, perms)
1830 os.chown(realpath, uid, gid)
1831-
1832-
1833-def write_file(path, content, owner='root', group='root', perms=0444):
1834- """Create or overwrite a file with the contents of a string"""
1835+ os.chmod(realpath, perms)
1836+
1837+
1838+def write_file(path, content, owner='root', group='root', perms=0o444):
1839+ """Create or overwrite a file with the contents of a byte string."""
1840 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1841 uid = pwd.getpwnam(owner).pw_uid
1842 gid = grp.getgrnam(group).gr_gid
1843- with open(path, 'w') as target:
1844+ with open(path, 'wb') as target:
1845 os.fchown(target.fileno(), uid, gid)
1846 os.fchmod(target.fileno(), perms)
1847 target.write(content)
1848@@ -177,7 +221,7 @@
1849 cmd_args.extend([device, mountpoint])
1850 try:
1851 subprocess.check_output(cmd_args)
1852- except subprocess.CalledProcessError, e:
1853+ except subprocess.CalledProcessError as e:
1854 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
1855 return False
1856
1857@@ -191,7 +235,7 @@
1858 cmd_args = ['umount', mountpoint]
1859 try:
1860 subprocess.check_output(cmd_args)
1861- except subprocess.CalledProcessError, e:
1862+ except subprocess.CalledProcessError as e:
1863 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1864 return False
1865
1866@@ -209,17 +253,42 @@
1867 return system_mounts
1868
1869
1870-def file_hash(path):
1871- """Generate a md5 hash of the contents of 'path' or None if not found """
1872+def file_hash(path, hash_type='md5'):
1873+ """
1874+ Generate a hash checksum of the contents of 'path' or None if not found.
1875+
1876+ :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
1877+ such as md5, sha1, sha256, sha512, etc.
1878+ """
1879 if os.path.exists(path):
1880- h = hashlib.md5()
1881- with open(path, 'r') as source:
1882- h.update(source.read()) # IGNORE:E1101 - it does have update
1883+ h = getattr(hashlib, hash_type)()
1884+ with open(path, 'rb') as source:
1885+ h.update(source.read())
1886 return h.hexdigest()
1887 else:
1888 return None
1889
1890
1891+def check_hash(path, checksum, hash_type='md5'):
1892+ """
1893+ Validate a file using a cryptographic checksum.
1894+
1895+ :param str checksum: Value of the checksum used to validate the file.
1896+ :param str hash_type: Hash algorithm used to generate `checksum`.
1897+ Can be any hash alrgorithm supported by :mod:`hashlib`,
1898+ such as md5, sha1, sha256, sha512, etc.
1899+ :raises ChecksumError: If the file fails the checksum
1900+
1901+ """
1902+ actual_checksum = file_hash(path, hash_type)
1903+ if checksum != actual_checksum:
1904+ raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
1905+
1906+
1907+class ChecksumError(ValueError):
1908+ pass
1909+
1910+
1911 def restart_on_change(restart_map, stopstart=False):
1912 """Restart services based on configuration files changing
1913
1914@@ -236,11 +305,11 @@
1915 ceph_client_changed function.
1916 """
1917 def wrap(f):
1918- def wrapped_f(*args):
1919+ def wrapped_f(*args, **kwargs):
1920 checksums = {}
1921 for path in restart_map:
1922 checksums[path] = file_hash(path)
1923- f(*args)
1924+ f(*args, **kwargs)
1925 restarts = []
1926 for path in restart_map:
1927 if checksums[path] != file_hash(path):
1928@@ -270,29 +339,39 @@
1929 def pwgen(length=None):
1930 """Generate a random pasword."""
1931 if length is None:
1932+ # A random length is ok to use a weak PRNG
1933 length = random.choice(range(35, 45))
1934 alphanumeric_chars = [
1935- l for l in (string.letters + string.digits)
1936+ l for l in (string.ascii_letters + string.digits)
1937 if l not in 'l0QD1vAEIOUaeiou']
1938+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
1939+ # actual password
1940+ random_generator = random.SystemRandom()
1941 random_chars = [
1942- random.choice(alphanumeric_chars) for _ in range(length)]
1943+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
1944 return(''.join(random_chars))
1945
1946
1947 def list_nics(nic_type):
1948 '''Return a list of nics of given type(s)'''
1949- if isinstance(nic_type, basestring):
1950+ if isinstance(nic_type, six.string_types):
1951 int_types = [nic_type]
1952 else:
1953 int_types = nic_type
1954 interfaces = []
1955 for int_type in int_types:
1956 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1957- ip_output = subprocess.check_output(cmd).split('\n')
1958+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1959 ip_output = (line for line in ip_output if line)
1960 for line in ip_output:
1961 if line.split()[1].startswith(int_type):
1962- interfaces.append(line.split()[1].replace(":", ""))
1963+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
1964+ if matched:
1965+ interface = matched.groups()[0]
1966+ else:
1967+ interface = line.split()[1].replace(":", "")
1968+ interfaces.append(interface)
1969+
1970 return interfaces
1971
1972
1973@@ -304,7 +383,7 @@
1974
1975 def get_nic_mtu(nic):
1976 cmd = ['ip', 'addr', 'show', nic]
1977- ip_output = subprocess.check_output(cmd).split('\n')
1978+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1979 mtu = ""
1980 for line in ip_output:
1981 words = line.split()
1982@@ -315,7 +394,7 @@
1983
1984 def get_nic_hwaddr(nic):
1985 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1986- ip_output = subprocess.check_output(cmd)
1987+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1988 hwaddr = ""
1989 words = ip_output.split()
1990 if 'link/ether' in words:
1991@@ -330,10 +409,13 @@
1992 * 0 => Installed revno is the same as supplied arg
1993 * -1 => Installed revno is less than supplied arg
1994
1995+ This function imports apt_cache function from charmhelpers.fetch if
1996+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1997+ you call this function, or pass an apt_pkg.Cache() instance.
1998 '''
1999 import apt_pkg
2000- from charmhelpers.fetch import apt_cache
2001 if not pkgcache:
2002+ from charmhelpers.fetch import apt_cache
2003 pkgcache = apt_cache()
2004 pkg = pkgcache[package]
2005 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
2006@@ -348,13 +430,21 @@
2007 os.chdir(cur)
2008
2009
2010-def chownr(path, owner, group):
2011+def chownr(path, owner, group, follow_links=True):
2012 uid = pwd.getpwnam(owner).pw_uid
2013 gid = grp.getgrnam(group).gr_gid
2014+ if follow_links:
2015+ chown = os.chown
2016+ else:
2017+ chown = os.lchown
2018
2019 for root, dirs, files in os.walk(path):
2020 for name in dirs + files:
2021 full = os.path.join(root, name)
2022 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
2023 if not broken_symlink:
2024- os.chown(full, uid, gid)
2025+ chown(full, uid, gid)
2026+
2027+
2028+def lchownr(path, owner, group):
2029+ chownr(path, owner, group, follow_links=False)
2030
2031=== added directory 'hooks/charmhelpers/core/services'
2032=== added file 'hooks/charmhelpers/core/services/__init__.py'
2033--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
2034+++ hooks/charmhelpers/core/services/__init__.py 2015-03-23 00:51:45 +0000
2035@@ -0,0 +1,18 @@
2036+# Copyright 2014-2015 Canonical Limited.
2037+#
2038+# This file is part of charm-helpers.
2039+#
2040+# charm-helpers is free software: you can redistribute it and/or modify
2041+# it under the terms of the GNU Lesser General Public License version 3 as
2042+# published by the Free Software Foundation.
2043+#
2044+# charm-helpers is distributed in the hope that it will be useful,
2045+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2046+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2047+# GNU Lesser General Public License for more details.
2048+#
2049+# You should have received a copy of the GNU Lesser General Public License
2050+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2051+
2052+from .base import * # NOQA
2053+from .helpers import * # NOQA
2054
2055=== added file 'hooks/charmhelpers/core/services/base.py'
2056--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
2057+++ hooks/charmhelpers/core/services/base.py 2015-03-23 00:51:45 +0000
2058@@ -0,0 +1,329 @@
2059+# Copyright 2014-2015 Canonical Limited.
2060+#
2061+# This file is part of charm-helpers.
2062+#
2063+# charm-helpers is free software: you can redistribute it and/or modify
2064+# it under the terms of the GNU Lesser General Public License version 3 as
2065+# published by the Free Software Foundation.
2066+#
2067+# charm-helpers is distributed in the hope that it will be useful,
2068+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2069+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2070+# GNU Lesser General Public License for more details.
2071+#
2072+# You should have received a copy of the GNU Lesser General Public License
2073+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2074+
2075+import os
2076+import re
2077+import json
2078+from collections import Iterable
2079+
2080+from charmhelpers.core import host
2081+from charmhelpers.core import hookenv
2082+
2083+
2084+__all__ = ['ServiceManager', 'ManagerCallback',
2085+ 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
2086+ 'service_restart', 'service_stop']
2087+
2088+
2089+class ServiceManager(object):
2090+ def __init__(self, services=None):
2091+ """
2092+ Register a list of services, given their definitions.
2093+
2094+ Service definitions are dicts in the following formats (all keys except
2095+ 'service' are optional)::
2096+
2097+ {
2098+ "service": <service name>,
2099+ "required_data": <list of required data contexts>,
2100+ "provided_data": <list of provided data contexts>,
2101+ "data_ready": <one or more callbacks>,
2102+ "data_lost": <one or more callbacks>,
2103+ "start": <one or more callbacks>,
2104+ "stop": <one or more callbacks>,
2105+ "ports": <list of ports to manage>,
2106+ }
2107+
2108+ The 'required_data' list should contain dicts of required data (or
2109+ dependency managers that act like dicts and know how to collect the data).
2110+ Only when all items in the 'required_data' list are populated are the list
2111+ of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
2112+ information.
2113+
2114+ The 'provided_data' list should contain relation data providers, most likely
2115+ a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
2116+ that will indicate a set of data to set on a given relation.
2117+
2118+ The 'data_ready' value should be either a single callback, or a list of
2119+ callbacks, to be called when all items in 'required_data' pass `is_ready()`.
2120+ Each callback will be called with the service name as the only parameter.
2121+ After all of the 'data_ready' callbacks are called, the 'start' callbacks
2122+ are fired.
2123+
2124+ The 'data_lost' value should be either a single callback, or a list of
2125+ callbacks, to be called when a 'required_data' item no longer passes
2126+ `is_ready()`. Each callback will be called with the service name as the
2127+ only parameter. After all of the 'data_lost' callbacks are called,
2128+ the 'stop' callbacks are fired.
2129+
2130+ The 'start' value should be either a single callback, or a list of
2131+ callbacks, to be called when starting the service, after the 'data_ready'
2132+ callbacks are complete. Each callback will be called with the service
2133+ name as the only parameter. This defaults to
2134+ `[host.service_start, services.open_ports]`.
2135+
2136+ The 'stop' value should be either a single callback, or a list of
2137+ callbacks, to be called when stopping the service. If the service is
2138+ being stopped because it no longer has all of its 'required_data', this
2139+ will be called after all of the 'data_lost' callbacks are complete.
2140+ Each callback will be called with the service name as the only parameter.
2141+ This defaults to `[services.close_ports, host.service_stop]`.
2142+
2143+ The 'ports' value should be a list of ports to manage. The default
2144+ 'start' handler will open the ports after the service is started,
2145+ and the default 'stop' handler will close the ports prior to stopping
2146+ the service.
2147+
2148+
2149+ Examples:
2150+
2151+ The following registers an Upstart service called bingod that depends on
2152+ a mongodb relation and which runs a custom `db_migrate` function prior to
2153+ restarting the service, and a Runit service called spadesd::
2154+
2155+ manager = services.ServiceManager([
2156+ {
2157+ 'service': 'bingod',
2158+ 'ports': [80, 443],
2159+ 'required_data': [MongoRelation(), config(), {'my': 'data'}],
2160+ 'data_ready': [
2161+ services.template(source='bingod.conf'),
2162+ services.template(source='bingod.ini',
2163+ target='/etc/bingod.ini',
2164+ owner='bingo', perms=0400),
2165+ ],
2166+ },
2167+ {
2168+ 'service': 'spadesd',
2169+ 'data_ready': services.template(source='spadesd_run.j2',
2170+ target='/etc/sv/spadesd/run',
2171+ perms=0555),
2172+ 'start': runit_start,
2173+ 'stop': runit_stop,
2174+ },
2175+ ])
2176+ manager.manage()
2177+ """
2178+ self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
2179+ self._ready = None
2180+ self.services = {}
2181+ for service in services or []:
2182+ service_name = service['service']
2183+ self.services[service_name] = service
2184+
2185+ def manage(self):
2186+ """
2187+ Handle the current hook by doing The Right Thing with the registered services.
2188+ """
2189+ hook_name = hookenv.hook_name()
2190+ if hook_name == 'stop':
2191+ self.stop_services()
2192+ else:
2193+ self.provide_data()
2194+ self.reconfigure_services()
2195+ cfg = hookenv.config()
2196+ if cfg.implicit_save:
2197+ cfg.save()
2198+
2199+ def provide_data(self):
2200+ """
2201+ Set the relation data for each provider in the ``provided_data`` list.
2202+
2203+ A provider must have a `name` attribute, which indicates which relation
2204+ to set data on, and a `provide_data()` method, which returns a dict of
2205+ data to set.
2206+ """
2207+ hook_name = hookenv.hook_name()
2208+ for service in self.services.values():
2209+ for provider in service.get('provided_data', []):
2210+ if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
2211+ data = provider.provide_data()
2212+ _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
2213+ if _ready:
2214+ hookenv.relation_set(None, data)
2215+
2216+ def reconfigure_services(self, *service_names):
2217+ """
2218+ Update all files for one or more registered services, and,
2219+ if ready, optionally restart them.
2220+
2221+ If no service names are given, reconfigures all registered services.
2222+ """
2223+ for service_name in service_names or self.services.keys():
2224+ if self.is_ready(service_name):
2225+ self.fire_event('data_ready', service_name)
2226+ self.fire_event('start', service_name, default=[
2227+ service_restart,
2228+ manage_ports])
2229+ self.save_ready(service_name)
2230+ else:
2231+ if self.was_ready(service_name):
2232+ self.fire_event('data_lost', service_name)
2233+ self.fire_event('stop', service_name, default=[
2234+ manage_ports,
2235+ service_stop])
2236+ self.save_lost(service_name)
2237+
2238+ def stop_services(self, *service_names):
2239+ """
2240+ Stop one or more registered services, by name.
2241+
2242+ If no service names are given, stops all registered services.
2243+ """
2244+ for service_name in service_names or self.services.keys():
2245+ self.fire_event('stop', service_name, default=[
2246+ manage_ports,
2247+ service_stop])
2248+
2249+ def get_service(self, service_name):
2250+ """
2251+ Given the name of a registered service, return its service definition.
2252+ """
2253+ service = self.services.get(service_name)
2254+ if not service:
2255+ raise KeyError('Service not registered: %s' % service_name)
2256+ return service
2257+
2258+ def fire_event(self, event_name, service_name, default=None):
2259+ """
2260+ Fire a data_ready, data_lost, start, or stop event on a given service.
2261+ """
2262+ service = self.get_service(service_name)
2263+ callbacks = service.get(event_name, default)
2264+ if not callbacks:
2265+ return
2266+ if not isinstance(callbacks, Iterable):
2267+ callbacks = [callbacks]
2268+ for callback in callbacks:
2269+ if isinstance(callback, ManagerCallback):
2270+ callback(self, service_name, event_name)
2271+ else:
2272+ callback(service_name)
2273+
2274+ def is_ready(self, service_name):
2275+ """
2276+ Determine if a registered service is ready, by checking its 'required_data'.
2277+
2278+ A 'required_data' item can be any mapping type, and is considered ready
2279+ if `bool(item)` evaluates as True.
2280+ """
2281+ service = self.get_service(service_name)
2282+ reqs = service.get('required_data', [])
2283+ return all(bool(req) for req in reqs)
2284+
2285+ def _load_ready_file(self):
2286+ if self._ready is not None:
2287+ return
2288+ if os.path.exists(self._ready_file):
2289+ with open(self._ready_file) as fp:
2290+ self._ready = set(json.load(fp))
2291+ else:
2292+ self._ready = set()
2293+
2294+ def _save_ready_file(self):
2295+ if self._ready is None:
2296+ return
2297+ with open(self._ready_file, 'w') as fp:
2298+ json.dump(list(self._ready), fp)
2299+
2300+ def save_ready(self, service_name):
2301+ """
2302+ Save an indicator that the given service is now data_ready.
2303+ """
2304+ self._load_ready_file()
2305+ self._ready.add(service_name)
2306+ self._save_ready_file()
2307+
2308+ def save_lost(self, service_name):
2309+ """
2310+ Save an indicator that the given service is no longer data_ready.
2311+ """
2312+ self._load_ready_file()
2313+ self._ready.discard(service_name)
2314+ self._save_ready_file()
2315+
2316+ def was_ready(self, service_name):
2317+ """
2318+ Determine if the given service was previously data_ready.
2319+ """
2320+ self._load_ready_file()
2321+ return service_name in self._ready
2322+
2323+
2324+class ManagerCallback(object):
2325+ """
2326+ Special case of a callback that takes the `ServiceManager` instance
2327+ in addition to the service name.
2328+
2329+ Subclasses should implement `__call__` which should accept three parameters:
2330+
2331+ * `manager` The `ServiceManager` instance
2332+ * `service_name` The name of the service it's being triggered for
2333+ * `event_name` The name of the event that this callback is handling
2334+ """
2335+ def __call__(self, manager, service_name, event_name):
2336+ raise NotImplementedError()
2337+
2338+
2339+class PortManagerCallback(ManagerCallback):
2340+ """
2341+ Callback class that will open or close ports, for use as either
2342+ a start or stop action.
2343+ """
2344+ def __call__(self, manager, service_name, event_name):
2345+ service = manager.get_service(service_name)
2346+ new_ports = service.get('ports', [])
2347+ port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
2348+ if os.path.exists(port_file):
2349+ with open(port_file) as fp:
2350+ old_ports = fp.read().split(',')
2351+ for old_port in old_ports:
2352+ if bool(old_port):
2353+ old_port = int(old_port)
2354+ if old_port not in new_ports:
2355+ hookenv.close_port(old_port)
2356+ with open(port_file, 'w') as fp:
2357+ fp.write(','.join(str(port) for port in new_ports))
2358+ for port in new_ports:
2359+ if event_name == 'start':
2360+ hookenv.open_port(port)
2361+ elif event_name == 'stop':
2362+ hookenv.close_port(port)
2363+
2364+
2365+def service_stop(service_name):
2366+ """
2367+ Wrapper around host.service_stop to prevent spurious "unknown service"
2368+ messages in the logs.
2369+ """
2370+ if host.service_running(service_name):
2371+ host.service_stop(service_name)
2372+
2373+
2374+def service_restart(service_name):
2375+ """
2376+ Wrapper around host.service_restart to prevent spurious "unknown service"
2377+ messages in the logs.
2378+ """
2379+ if host.service_available(service_name):
2380+ if host.service_running(service_name):
2381+ host.service_restart(service_name)
2382+ else:
2383+ host.service_start(service_name)
2384+
2385+
2386+# Convenience aliases
2387+open_ports = close_ports = manage_ports = PortManagerCallback()
2388
2389=== added file 'hooks/charmhelpers/core/services/helpers.py'
2390--- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
2391+++ hooks/charmhelpers/core/services/helpers.py 2015-03-23 00:51:45 +0000
2392@@ -0,0 +1,267 @@
2393+# Copyright 2014-2015 Canonical Limited.
2394+#
2395+# This file is part of charm-helpers.
2396+#
2397+# charm-helpers is free software: you can redistribute it and/or modify
2398+# it under the terms of the GNU Lesser General Public License version 3 as
2399+# published by the Free Software Foundation.
2400+#
2401+# charm-helpers is distributed in the hope that it will be useful,
2402+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2403+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2404+# GNU Lesser General Public License for more details.
2405+#
2406+# You should have received a copy of the GNU Lesser General Public License
2407+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2408+
2409+import os
2410+import yaml
2411+from charmhelpers.core import hookenv
2412+from charmhelpers.core import templating
2413+
2414+from charmhelpers.core.services.base import ManagerCallback
2415+
2416+
2417+__all__ = ['RelationContext', 'TemplateCallback',
2418+ 'render_template', 'template']
2419+
2420+
2421+class RelationContext(dict):
2422+ """
2423+ Base class for a context generator that gets relation data from juju.
2424+
2425+ Subclasses must provide the attributes `name`, which is the name of the
2426+ interface of interest, `interface`, which is the type of the interface of
2427+ interest, and `required_keys`, which is the set of keys required for the
2428+ relation to be considered complete. The data for all interfaces matching
2429+ the `name` attribute that are complete will used to populate the dictionary
2430+ values (see `get_data`, below).
2431+
2432+ The generated context will be namespaced under the relation :attr:`name`,
2433+ to prevent potential naming conflicts.
2434+
2435+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2436+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2437+ """
2438+ name = None
2439+ interface = None
2440+
2441+ def __init__(self, name=None, additional_required_keys=None):
2442+ if not hasattr(self, 'required_keys'):
2443+ self.required_keys = []
2444+
2445+ if name is not None:
2446+ self.name = name
2447+ if additional_required_keys:
2448+ self.required_keys.extend(additional_required_keys)
2449+ self.get_data()
2450+
2451+ def __bool__(self):
2452+ """
2453+ Returns True if all of the required_keys are available.
2454+ """
2455+ return self.is_ready()
2456+
2457+ __nonzero__ = __bool__
2458+
2459+ def __repr__(self):
2460+ return super(RelationContext, self).__repr__()
2461+
2462+ def is_ready(self):
2463+ """
2464+ Returns True if all of the `required_keys` are available from any units.
2465+ """
2466+ ready = len(self.get(self.name, [])) > 0
2467+ if not ready:
2468+ hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
2469+ return ready
2470+
2471+ def _is_ready(self, unit_data):
2472+ """
2473+ Helper method that tests a set of relation data and returns True if
2474+ all of the `required_keys` are present.
2475+ """
2476+ return set(unit_data.keys()).issuperset(set(self.required_keys))
2477+
2478+ def get_data(self):
2479+ """
2480+ Retrieve the relation data for each unit involved in a relation and,
2481+ if complete, store it in a list under `self[self.name]`. This
2482+ is automatically called when the RelationContext is instantiated.
2483+
2484+ The units are sorted lexographically first by the service ID, then by
2485+ the unit ID. Thus, if an interface has two other services, 'db:1'
2486+ and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
2487+ and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
2488+ set of data, the relation data for the units will be stored in the
2489+ order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
2490+
2491+ If you only care about a single unit on the relation, you can just
2492+ access it as `{{ interface[0]['key'] }}`. However, if you can at all
2493+ support multiple units on a relation, you should iterate over the list,
2494+ like::
2495+
2496+ {% for unit in interface -%}
2497+ {{ unit['key'] }}{% if not loop.last %},{% endif %}
2498+ {%- endfor %}
2499+
2500+ Note that since all sets of relation data from all related services and
2501+ units are in a single list, if you need to know which service or unit a
2502+ set of data came from, you'll need to extend this class to preserve
2503+ that information.
2504+ """
2505+ if not hookenv.relation_ids(self.name):
2506+ return
2507+
2508+ ns = self.setdefault(self.name, [])
2509+ for rid in sorted(hookenv.relation_ids(self.name)):
2510+ for unit in sorted(hookenv.related_units(rid)):
2511+ reldata = hookenv.relation_get(rid=rid, unit=unit)
2512+ if self._is_ready(reldata):
2513+ ns.append(reldata)
2514+
2515+ def provide_data(self):
2516+ """
2517+ Return data to be relation_set for this interface.
2518+ """
2519+ return {}
2520+
2521+
2522+class MysqlRelation(RelationContext):
2523+ """
2524+ Relation context for the `mysql` interface.
2525+
2526+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2527+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2528+ """
2529+ name = 'db'
2530+ interface = 'mysql'
2531+
2532+ def __init__(self, *args, **kwargs):
2533+ self.required_keys = ['host', 'user', 'password', 'database']
2534+ super(HttpRelation).__init__(self, *args, **kwargs)
2535+
2536+
2537+class HttpRelation(RelationContext):
2538+ """
2539+ Relation context for the `http` interface.
2540+
2541+ :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
2542+ :param list additional_required_keys: Extend the list of :attr:`required_keys`
2543+ """
2544+ name = 'website'
2545+ interface = 'http'
2546+
2547+ def __init__(self, *args, **kwargs):
2548+ self.required_keys = ['host', 'port']
2549+ super(HttpRelation).__init__(self, *args, **kwargs)
2550+
2551+ def provide_data(self):
2552+ return {
2553+ 'host': hookenv.unit_get('private-address'),
2554+ 'port': 80,
2555+ }
2556+
2557+
2558+class RequiredConfig(dict):
2559+ """
2560+ Data context that loads config options with one or more mandatory options.
2561+
2562+ Once the required options have been changed from their default values, all
2563+ config options will be available, namespaced under `config` to prevent
2564+ potential naming conflicts (for example, between a config option and a
2565+ relation property).
2566+
2567+ :param list *args: List of options that must be changed from their default values.
2568+ """
2569+
2570+ def __init__(self, *args):
2571+ self.required_options = args
2572+ self['config'] = hookenv.config()
2573+ with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
2574+ self.config = yaml.load(fp).get('options', {})
2575+
2576+ def __bool__(self):
2577+ for option in self.required_options:
2578+ if option not in self['config']:
2579+ return False
2580+ current_value = self['config'][option]
2581+ default_value = self.config[option].get('default')
2582+ if current_value == default_value:
2583+ return False
2584+ if current_value in (None, '') and default_value in (None, ''):
2585+ return False
2586+ return True
2587+
2588+ def __nonzero__(self):
2589+ return self.__bool__()
2590+
2591+
2592+class StoredContext(dict):
2593+ """
2594+ A data context that always returns the data that it was first created with.
2595+
2596+ This is useful to do a one-time generation of things like passwords, that
2597+ will thereafter use the same value that was originally generated, instead
2598+ of generating a new value each time it is run.
2599+ """
2600+ def __init__(self, file_name, config_data):
2601+ """
2602+ If the file exists, populate `self` with the data from the file.
2603+ Otherwise, populate with the given data and persist it to the file.
2604+ """
2605+ if os.path.exists(file_name):
2606+ self.update(self.read_context(file_name))
2607+ else:
2608+ self.store_context(file_name, config_data)
2609+ self.update(config_data)
2610+
2611+ def store_context(self, file_name, config_data):
2612+ if not os.path.isabs(file_name):
2613+ file_name = os.path.join(hookenv.charm_dir(), file_name)
2614+ with open(file_name, 'w') as file_stream:
2615+ os.fchmod(file_stream.fileno(), 0o600)
2616+ yaml.dump(config_data, file_stream)
2617+
2618+ def read_context(self, file_name):
2619+ if not os.path.isabs(file_name):
2620+ file_name = os.path.join(hookenv.charm_dir(), file_name)
2621+ with open(file_name, 'r') as file_stream:
2622+ data = yaml.load(file_stream)
2623+ if not data:
2624+ raise OSError("%s is empty" % file_name)
2625+ return data
2626+
2627+
2628+class TemplateCallback(ManagerCallback):
2629+ """
2630+ Callback class that will render a Jinja2 template, for use as a ready
2631+ action.
2632+
2633+ :param str source: The template source file, relative to
2634+ `$CHARM_DIR/templates`
2635+
2636+ :param str target: The target to write the rendered template to
2637+ :param str owner: The owner of the rendered file
2638+ :param str group: The group of the rendered file
2639+ :param int perms: The permissions of the rendered file
2640+ """
2641+ def __init__(self, source, target,
2642+ owner='root', group='root', perms=0o444):
2643+ self.source = source
2644+ self.target = target
2645+ self.owner = owner
2646+ self.group = group
2647+ self.perms = perms
2648+
2649+ def __call__(self, manager, service_name, event_name):
2650+ service = manager.get_service(service_name)
2651+ context = {}
2652+ for ctx in service.get('required_data', []):
2653+ context.update(ctx)
2654+ templating.render(self.source, self.target, context,
2655+ self.owner, self.group, self.perms)
2656+
2657+
2658+# Convenience aliases for templates
2659+render_template = template = TemplateCallback
2660
2661=== added file 'hooks/charmhelpers/core/strutils.py'
2662--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
2663+++ hooks/charmhelpers/core/strutils.py 2015-03-23 00:51:45 +0000
2664@@ -0,0 +1,42 @@
2665+#!/usr/bin/env python
2666+# -*- coding: utf-8 -*-
2667+
2668+# Copyright 2014-2015 Canonical Limited.
2669+#
2670+# This file is part of charm-helpers.
2671+#
2672+# charm-helpers is free software: you can redistribute it and/or modify
2673+# it under the terms of the GNU Lesser General Public License version 3 as
2674+# published by the Free Software Foundation.
2675+#
2676+# charm-helpers is distributed in the hope that it will be useful,
2677+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2678+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2679+# GNU Lesser General Public License for more details.
2680+#
2681+# You should have received a copy of the GNU Lesser General Public License
2682+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2683+
2684+import six
2685+
2686+
2687+def bool_from_string(value):
2688+ """Interpret string value as boolean.
2689+
2690+ Returns True if value translates to True otherwise False.
2691+ """
2692+ if isinstance(value, six.string_types):
2693+ value = six.text_type(value)
2694+ else:
2695+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2696+ raise ValueError(msg)
2697+
2698+ value = value.strip().lower()
2699+
2700+ if value in ['y', 'yes', 'true', 't']:
2701+ return True
2702+ elif value in ['n', 'no', 'false', 'f']:
2703+ return False
2704+
2705+ msg = "Unable to interpret string value '%s' as boolean" % (value)
2706+ raise ValueError(msg)
2707
2708=== added file 'hooks/charmhelpers/core/sysctl.py'
2709--- hooks/charmhelpers/core/sysctl.py 1970-01-01 00:00:00 +0000
2710+++ hooks/charmhelpers/core/sysctl.py 2015-03-23 00:51:45 +0000
2711@@ -0,0 +1,56 @@
2712+#!/usr/bin/env python
2713+# -*- coding: utf-8 -*-
2714+
2715+# Copyright 2014-2015 Canonical Limited.
2716+#
2717+# This file is part of charm-helpers.
2718+#
2719+# charm-helpers is free software: you can redistribute it and/or modify
2720+# it under the terms of the GNU Lesser General Public License version 3 as
2721+# published by the Free Software Foundation.
2722+#
2723+# charm-helpers is distributed in the hope that it will be useful,
2724+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2725+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2726+# GNU Lesser General Public License for more details.
2727+#
2728+# You should have received a copy of the GNU Lesser General Public License
2729+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2730+
2731+import yaml
2732+
2733+from subprocess import check_call
2734+
2735+from charmhelpers.core.hookenv import (
2736+ log,
2737+ DEBUG,
2738+ ERROR,
2739+)
2740+
2741+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2742+
2743+
2744+def create(sysctl_dict, sysctl_file):
2745+ """Creates a sysctl.conf file from a YAML associative array
2746+
2747+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
2748+ :type sysctl_dict: str
2749+ :param sysctl_file: path to the sysctl file to be saved
2750+ :type sysctl_file: str or unicode
2751+ :returns: None
2752+ """
2753+ try:
2754+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
2755+ except yaml.YAMLError:
2756+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
2757+ level=ERROR)
2758+ return
2759+
2760+ with open(sysctl_file, "w") as fd:
2761+ for key, value in sysctl_dict_parsed.items():
2762+ fd.write("{}={}\n".format(key, value))
2763+
2764+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
2765+ level=DEBUG)
2766+
2767+ check_call(["sysctl", "-p", sysctl_file])
2768
2769=== added file 'hooks/charmhelpers/core/templating.py'
2770--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
2771+++ hooks/charmhelpers/core/templating.py 2015-03-23 00:51:45 +0000
2772@@ -0,0 +1,68 @@
2773+# Copyright 2014-2015 Canonical Limited.
2774+#
2775+# This file is part of charm-helpers.
2776+#
2777+# charm-helpers is free software: you can redistribute it and/or modify
2778+# it under the terms of the GNU Lesser General Public License version 3 as
2779+# published by the Free Software Foundation.
2780+#
2781+# charm-helpers is distributed in the hope that it will be useful,
2782+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2783+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2784+# GNU Lesser General Public License for more details.
2785+#
2786+# You should have received a copy of the GNU Lesser General Public License
2787+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2788+
2789+import os
2790+
2791+from charmhelpers.core import host
2792+from charmhelpers.core import hookenv
2793+
2794+
2795+def render(source, target, context, owner='root', group='root',
2796+ perms=0o444, templates_dir=None, encoding='UTF-8'):
2797+ """
2798+ Render a template.
2799+
2800+ The `source` path, if not absolute, is relative to the `templates_dir`.
2801+
2802+ The `target` path should be absolute.
2803+
2804+ The context should be a dict containing the values to be replaced in the
2805+ template.
2806+
2807+ The `owner`, `group`, and `perms` options will be passed to `write_file`.
2808+
2809+ If omitted, `templates_dir` defaults to the `templates` folder in the charm.
2810+
2811+ Note: Using this requires python-jinja2; if it is not installed, calling
2812+ this will attempt to use charmhelpers.fetch.apt_install to install it.
2813+ """
2814+ try:
2815+ from jinja2 import FileSystemLoader, Environment, exceptions
2816+ except ImportError:
2817+ try:
2818+ from charmhelpers.fetch import apt_install
2819+ except ImportError:
2820+ hookenv.log('Could not import jinja2, and could not import '
2821+ 'charmhelpers.fetch to install it',
2822+ level=hookenv.ERROR)
2823+ raise
2824+ apt_install('python-jinja2', fatal=True)
2825+ from jinja2 import FileSystemLoader, Environment, exceptions
2826+
2827+ if templates_dir is None:
2828+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2829+ loader = Environment(loader=FileSystemLoader(templates_dir))
2830+ try:
2831+ source = source
2832+ template = loader.get_template(source)
2833+ except exceptions.TemplateNotFound as e:
2834+ hookenv.log('Could not load template %s from %s.' %
2835+ (source, templates_dir),
2836+ level=hookenv.ERROR)
2837+ raise e
2838+ content = template.render(context)
2839+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2840+ host.write_file(target, content.encode(encoding), owner, group, perms)
2841
2842=== added file 'hooks/charmhelpers/core/unitdata.py'
2843--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
2844+++ hooks/charmhelpers/core/unitdata.py 2015-03-23 00:51:45 +0000
2845@@ -0,0 +1,477 @@
2846+#!/usr/bin/env python
2847+# -*- coding: utf-8 -*-
2848+#
2849+# Copyright 2014-2015 Canonical Limited.
2850+#
2851+# This file is part of charm-helpers.
2852+#
2853+# charm-helpers is free software: you can redistribute it and/or modify
2854+# it under the terms of the GNU Lesser General Public License version 3 as
2855+# published by the Free Software Foundation.
2856+#
2857+# charm-helpers is distributed in the hope that it will be useful,
2858+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2859+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2860+# GNU Lesser General Public License for more details.
2861+#
2862+# You should have received a copy of the GNU Lesser General Public License
2863+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2864+#
2865+#
2866+# Authors:
2867+# Kapil Thangavelu <kapil.foss@gmail.com>
2868+#
2869+"""
2870+Intro
2871+-----
2872+
2873+A simple way to store state in units. This provides a key value
2874+storage with support for versioned, transactional operation,
2875+and can calculate deltas from previous values to simplify unit logic
2876+when processing changes.
2877+
2878+
2879+Hook Integration
2880+----------------
2881+
2882+There are several extant frameworks for hook execution, including
2883+
2884+ - charmhelpers.core.hookenv.Hooks
2885+ - charmhelpers.core.services.ServiceManager
2886+
2887+The storage classes are framework agnostic, one simple integration is
2888+via the HookData contextmanager. It will record the current hook
2889+execution environment (including relation data, config data, etc.),
2890+setup a transaction and allow easy access to the changes from
2891+previously seen values. One consequence of the integration is the
2892+reservation of particular keys ('rels', 'unit', 'env', 'config',
2893+'charm_revisions') for their respective values.
2894+
2895+Here's a fully worked integration example using hookenv.Hooks::
2896+
2897+ from charmhelper.core import hookenv, unitdata
2898+
2899+ hook_data = unitdata.HookData()
2900+ db = unitdata.kv()
2901+ hooks = hookenv.Hooks()
2902+
2903+ @hooks.hook
2904+ def config_changed():
2905+ # Print all changes to configuration from previously seen
2906+ # values.
2907+ for changed, (prev, cur) in hook_data.conf.items():
2908+ print('config changed', changed,
2909+ 'previous value', prev,
2910+ 'current value', cur)
2911+
2912+ # Get some unit specific bookeeping
2913+ if not db.get('pkg_key'):
2914+ key = urllib.urlopen('https://example.com/pkg_key').read()
2915+ db.set('pkg_key', key)
2916+
2917+ # Directly access all charm config as a mapping.
2918+ conf = db.getrange('config', True)
2919+
2920+ # Directly access all relation data as a mapping
2921+ rels = db.getrange('rels', True)
2922+
2923+ if __name__ == '__main__':
2924+ with hook_data():
2925+ hook.execute()
2926+
2927+
2928+A more basic integration is via the hook_scope context manager which simply
2929+manages transaction scope (and records hook name, and timestamp)::
2930+
2931+ >>> from unitdata import kv
2932+ >>> db = kv()
2933+ >>> with db.hook_scope('install'):
2934+ ... # do work, in transactional scope.
2935+ ... db.set('x', 1)
2936+ >>> db.get('x')
2937+ 1
2938+
2939+
2940+Usage
2941+-----
2942+
2943+Values are automatically json de/serialized to preserve basic typing
2944+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
2945+
2946+Individual values can be manipulated via get/set::
2947+
2948+ >>> kv.set('y', True)
2949+ >>> kv.get('y')
2950+ True
2951+
2952+ # We can set complex values (dicts, lists) as a single key.
2953+ >>> kv.set('config', {'a': 1, 'b': True'})
2954+
2955+ # Also supports returning dictionaries as a record which
2956+ # provides attribute access.
2957+ >>> config = kv.get('config', record=True)
2958+ >>> config.b
2959+ True
2960+
2961+
2962+Groups of keys can be manipulated with update/getrange::
2963+
2964+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
2965+ >>> kv.getrange('gui.', strip=True)
2966+ {'z': 1, 'y': 2}
2967+
2968+When updating values, its very helpful to understand which values
2969+have actually changed and how have they changed. The storage
2970+provides a delta method to provide for this::
2971+
2972+ >>> data = {'debug': True, 'option': 2}
2973+ >>> delta = kv.delta(data, 'config.')
2974+ >>> delta.debug.previous
2975+ None
2976+ >>> delta.debug.current
2977+ True
2978+ >>> delta
2979+ {'debug': (None, True), 'option': (None, 2)}
2980+
2981+Note the delta method does not persist the actual change, it needs to
2982+be explicitly saved via 'update' method::
2983+
2984+ >>> kv.update(data, 'config.')
2985+
2986+Values modified in the context of a hook scope retain historical values
2987+associated to the hookname.
2988+
2989+ >>> with db.hook_scope('config-changed'):
2990+ ... db.set('x', 42)
2991+ >>> db.gethistory('x')
2992+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
2993+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
2994+
2995+"""
2996+
2997+import collections
2998+import contextlib
2999+import datetime
3000+import json
3001+import os
3002+import pprint
3003+import sqlite3
3004+import sys
3005+
3006+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
3007+
3008+
3009+class Storage(object):
3010+ """Simple key value database for local unit state within charms.
3011+
3012+ Modifications are automatically committed at hook exit. That's
3013+ currently regardless of exit code.
3014+
3015+ To support dicts, lists, integer, floats, and booleans values
3016+ are automatically json encoded/decoded.
3017+ """
3018+ def __init__(self, path=None):
3019+ self.db_path = path
3020+ if path is None:
3021+ self.db_path = os.path.join(
3022+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
3023+ self.conn = sqlite3.connect('%s' % self.db_path)
3024+ self.cursor = self.conn.cursor()
3025+ self.revision = None
3026+ self._closed = False
3027+ self._init()
3028+
3029+ def close(self):
3030+ if self._closed:
3031+ return
3032+ self.flush(False)
3033+ self.cursor.close()
3034+ self.conn.close()
3035+ self._closed = True
3036+
3037+ def _scoped_query(self, stmt, params=None):
3038+ if params is None:
3039+ params = []
3040+ return stmt, params
3041+
3042+ def get(self, key, default=None, record=False):
3043+ self.cursor.execute(
3044+ *self._scoped_query(
3045+ 'select data from kv where key=?', [key]))
3046+ result = self.cursor.fetchone()
3047+ if not result:
3048+ return default
3049+ if record:
3050+ return Record(json.loads(result[0]))
3051+ return json.loads(result[0])
3052+
3053+ def getrange(self, key_prefix, strip=False):
3054+ stmt = "select key, data from kv where key like '%s%%'" % key_prefix
3055+ self.cursor.execute(*self._scoped_query(stmt))
3056+ result = self.cursor.fetchall()
3057+
3058+ if not result:
3059+ return None
3060+ if not strip:
3061+ key_prefix = ''
3062+ return dict([
3063+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
3064+
3065+ def update(self, mapping, prefix=""):
3066+ for k, v in mapping.items():
3067+ self.set("%s%s" % (prefix, k), v)
3068+
3069+ def unset(self, key):
3070+ self.cursor.execute('delete from kv where key=?', [key])
3071+ if self.revision and self.cursor.rowcount:
3072+ self.cursor.execute(
3073+ 'insert into kv_revisions values (?, ?, ?)',
3074+ [key, self.revision, json.dumps('DELETED')])
3075+
3076+ def set(self, key, value):
3077+ serialized = json.dumps(value)
3078+
3079+ self.cursor.execute(
3080+ 'select data from kv where key=?', [key])
3081+ exists = self.cursor.fetchone()
3082+
3083+ # Skip mutations to the same value
3084+ if exists:
3085+ if exists[0] == serialized:
3086+ return value
3087+
3088+ if not exists:
3089+ self.cursor.execute(
3090+ 'insert into kv (key, data) values (?, ?)',
3091+ (key, serialized))
3092+ else:
3093+ self.cursor.execute('''
3094+ update kv
3095+ set data = ?
3096+ where key = ?''', [serialized, key])
3097+
3098+ # Save
3099+ if not self.revision:
3100+ return value
3101+
3102+ self.cursor.execute(
3103+ 'select 1 from kv_revisions where key=? and revision=?',
3104+ [key, self.revision])
3105+ exists = self.cursor.fetchone()
3106+
3107+ if not exists:
3108+ self.cursor.execute(
3109+ '''insert into kv_revisions (
3110+ revision, key, data) values (?, ?, ?)''',
3111+ (self.revision, key, serialized))
3112+ else:
3113+ self.cursor.execute(
3114+ '''
3115+ update kv_revisions
3116+ set data = ?
3117+ where key = ?
3118+ and revision = ?''',
3119+ [serialized, key, self.revision])
3120+
3121+ return value
3122+
3123+ def delta(self, mapping, prefix):
3124+ """
3125+ return a delta containing values that have changed.
3126+ """
3127+ previous = self.getrange(prefix, strip=True)
3128+ if not previous:
3129+ pk = set()
3130+ else:
3131+ pk = set(previous.keys())
3132+ ck = set(mapping.keys())
3133+ delta = DeltaSet()
3134+
3135+ # added
3136+ for k in ck.difference(pk):
3137+ delta[k] = Delta(None, mapping[k])
3138+
3139+ # removed
3140+ for k in pk.difference(ck):
3141+ delta[k] = Delta(previous[k], None)
3142+
3143+ # changed
3144+ for k in pk.intersection(ck):
3145+ c = mapping[k]
3146+ p = previous[k]
3147+ if c != p:
3148+ delta[k] = Delta(p, c)
3149+
3150+ return delta
3151+
3152+ @contextlib.contextmanager
3153+ def hook_scope(self, name=""):
3154+ """Scope all future interactions to the current hook execution
3155+ revision."""
3156+ assert not self.revision
3157+ self.cursor.execute(
3158+ 'insert into hooks (hook, date) values (?, ?)',
3159+ (name or sys.argv[0],
3160+ datetime.datetime.utcnow().isoformat()))
3161+ self.revision = self.cursor.lastrowid
3162+ try:
3163+ yield self.revision
3164+ self.revision = None
3165+ except:
3166+ self.flush(False)
3167+ self.revision = None
3168+ raise
3169+ else:
3170+ self.flush()
3171+
3172+ def flush(self, save=True):
3173+ if save:
3174+ self.conn.commit()
3175+ elif self._closed:
3176+ return
3177+ else:
3178+ self.conn.rollback()
3179+
3180+ def _init(self):
3181+ self.cursor.execute('''
3182+ create table if not exists kv (
3183+ key text,
3184+ data text,
3185+ primary key (key)
3186+ )''')
3187+ self.cursor.execute('''
3188+ create table if not exists kv_revisions (
3189+ key text,
3190+ revision integer,
3191+ data text,
3192+ primary key (key, revision)
3193+ )''')
3194+ self.cursor.execute('''
3195+ create table if not exists hooks (
3196+ version integer primary key autoincrement,
3197+ hook text,
3198+ date text
3199+ )''')
3200+ self.conn.commit()
3201+
3202+ def gethistory(self, key, deserialize=False):
3203+ self.cursor.execute(
3204+ '''
3205+ select kv.revision, kv.key, kv.data, h.hook, h.date
3206+ from kv_revisions kv,
3207+ hooks h
3208+ where kv.key=?
3209+ and kv.revision = h.version
3210+ ''', [key])
3211+ if deserialize is False:
3212+ return self.cursor.fetchall()
3213+ return map(_parse_history, self.cursor.fetchall())
3214+
3215+ def debug(self, fh=sys.stderr):
3216+ self.cursor.execute('select * from kv')
3217+ pprint.pprint(self.cursor.fetchall(), stream=fh)
3218+ self.cursor.execute('select * from kv_revisions')
3219+ pprint.pprint(self.cursor.fetchall(), stream=fh)
3220+
3221+
3222+def _parse_history(d):
3223+ return (d[0], d[1], json.loads(d[2]), d[3],
3224+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
3225+
3226+
3227+class HookData(object):
3228+ """Simple integration for existing hook exec frameworks.
3229+
3230+ Records all unit information, and stores deltas for processing
3231+ by the hook.
3232+
3233+ Sample::
3234+
3235+ from charmhelper.core import hookenv, unitdata
3236+
3237+ changes = unitdata.HookData()
3238+ db = unitdata.kv()
3239+ hooks = hookenv.Hooks()
3240+
3241+ @hooks.hook
3242+ def config_changed():
3243+ # View all changes to configuration
3244+ for changed, (prev, cur) in changes.conf.items():
3245+ print('config changed', changed,
3246+ 'previous value', prev,
3247+ 'current value', cur)
3248+
3249+ # Get some unit specific bookeeping
3250+ if not db.get('pkg_key'):
3251+ key = urllib.urlopen('https://example.com/pkg_key').read()
3252+ db.set('pkg_key', key)
3253+
3254+ if __name__ == '__main__':
3255+ with changes():
3256+ hook.execute()
3257+
3258+ """
3259+ def __init__(self):
3260+ self.kv = kv()
3261+ self.conf = None
3262+ self.rels = None
3263+
3264+ @contextlib.contextmanager
3265+ def __call__(self):
3266+ from charmhelpers.core import hookenv
3267+ hook_name = hookenv.hook_name()
3268+
3269+ with self.kv.hook_scope(hook_name):
3270+ self._record_charm_version(hookenv.charm_dir())
3271+ delta_config, delta_relation = self._record_hook(hookenv)
3272+ yield self.kv, delta_config, delta_relation
3273+
3274+ def _record_charm_version(self, charm_dir):
3275+ # Record revisions.. charm revisions are meaningless
3276+ # to charm authors as they don't control the revision.
3277+ # so logic dependnent on revision is not particularly
3278+ # useful, however it is useful for debugging analysis.
3279+ charm_rev = open(
3280+ os.path.join(charm_dir, 'revision')).read().strip()
3281+ charm_rev = charm_rev or '0'
3282+ revs = self.kv.get('charm_revisions', [])
3283+ if charm_rev not in revs:
3284+ revs.append(charm_rev.strip() or '0')
3285+ self.kv.set('charm_revisions', revs)
3286+
3287+ def _record_hook(self, hookenv):
3288+ data = hookenv.execution_environment()
3289+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
3290+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
3291+ self.kv.set('env', data['env'])
3292+ self.kv.set('unit', data['unit'])
3293+ self.kv.set('relid', data.get('relid'))
3294+ return conf_delta, rels_delta
3295+
3296+
3297+class Record(dict):
3298+
3299+ __slots__ = ()
3300+
3301+ def __getattr__(self, k):
3302+ if k in self:
3303+ return self[k]
3304+ raise AttributeError(k)
3305+
3306+
3307+class DeltaSet(Record):
3308+
3309+ __slots__ = ()
3310+
3311+
3312+Delta = collections.namedtuple('Delta', ['previous', 'current'])
3313+
3314+
3315+_KV = None
3316+
3317+
3318+def kv():
3319+ global _KV
3320+ if _KV is None:
3321+ _KV = Storage()
3322+ return _KV
3323
3324=== modified file 'hooks/charmhelpers/fetch/__init__.py'
3325--- hooks/charmhelpers/fetch/__init__.py 2014-09-08 16:38:59 +0000
3326+++ hooks/charmhelpers/fetch/__init__.py 2015-03-23 00:51:45 +0000
3327@@ -1,3 +1,19 @@
3328+# Copyright 2014-2015 Canonical Limited.
3329+#
3330+# This file is part of charm-helpers.
3331+#
3332+# charm-helpers is free software: you can redistribute it and/or modify
3333+# it under the terms of the GNU Lesser General Public License version 3 as
3334+# published by the Free Software Foundation.
3335+#
3336+# charm-helpers is distributed in the hope that it will be useful,
3337+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3338+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3339+# GNU Lesser General Public License for more details.
3340+#
3341+# You should have received a copy of the GNU Lesser General Public License
3342+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3343+
3344 import importlib
3345 from tempfile import NamedTemporaryFile
3346 import time
3347@@ -5,10 +21,6 @@
3348 from charmhelpers.core.host import (
3349 lsb_release
3350 )
3351-from urlparse import (
3352- urlparse,
3353- urlunparse,
3354-)
3355 import subprocess
3356 from charmhelpers.core.hookenv import (
3357 config,
3358@@ -16,6 +28,12 @@
3359 )
3360 import os
3361
3362+import six
3363+if six.PY3:
3364+ from urllib.parse import urlparse, urlunparse
3365+else:
3366+ from urlparse import urlparse, urlunparse
3367+
3368
3369 CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
3370 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
3371@@ -62,9 +80,16 @@
3372 'trusty-juno/updates': 'trusty-updates/juno',
3373 'trusty-updates/juno': 'trusty-updates/juno',
3374 'juno/proposed': 'trusty-proposed/juno',
3375- 'juno/proposed': 'trusty-proposed/juno',
3376 'trusty-juno/proposed': 'trusty-proposed/juno',
3377 'trusty-proposed/juno': 'trusty-proposed/juno',
3378+ # Kilo
3379+ 'kilo': 'trusty-updates/kilo',
3380+ 'trusty-kilo': 'trusty-updates/kilo',
3381+ 'trusty-kilo/updates': 'trusty-updates/kilo',
3382+ 'trusty-updates/kilo': 'trusty-updates/kilo',
3383+ 'kilo/proposed': 'trusty-proposed/kilo',
3384+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
3385+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
3386 }
3387
3388 # The order of this list is very important. Handlers should be listed in from
3389@@ -72,6 +97,7 @@
3390 FETCH_HANDLERS = (
3391 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
3392 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
3393+ 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
3394 )
3395
3396 APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
3397@@ -148,7 +174,7 @@
3398 cmd = ['apt-get', '--assume-yes']
3399 cmd.extend(options)
3400 cmd.append('install')
3401- if isinstance(packages, basestring):
3402+ if isinstance(packages, six.string_types):
3403 cmd.append(packages)
3404 else:
3405 cmd.extend(packages)
3406@@ -181,7 +207,7 @@
3407 def apt_purge(packages, fatal=False):
3408 """Purge one or more packages"""
3409 cmd = ['apt-get', '--assume-yes', 'purge']
3410- if isinstance(packages, basestring):
3411+ if isinstance(packages, six.string_types):
3412 cmd.append(packages)
3413 else:
3414 cmd.extend(packages)
3415@@ -192,7 +218,7 @@
3416 def apt_hold(packages, fatal=False):
3417 """Hold one or more packages"""
3418 cmd = ['apt-mark', 'hold']
3419- if isinstance(packages, basestring):
3420+ if isinstance(packages, six.string_types):
3421 cmd.append(packages)
3422 else:
3423 cmd.extend(packages)
3424@@ -208,7 +234,8 @@
3425 """Add a package source to this system.
3426
3427 @param source: a URL or sources.list entry, as supported by
3428- add-apt-repository(1). Examples:
3429+ add-apt-repository(1). Examples::
3430+
3431 ppa:charmers/example
3432 deb https://stub:key@private.example.com/ubuntu trusty main
3433
3434@@ -217,6 +244,7 @@
3435 pocket for the release.
3436 'cloud:' may be used to activate official cloud archive pockets,
3437 such as 'cloud:icehouse'
3438+ 'distro' may be used as a noop
3439
3440 @param key: A key to be added to the system's APT keyring and used
3441 to verify the signatures on packages. Ideally, this should be an
3442@@ -250,12 +278,14 @@
3443 release = lsb_release()['DISTRIB_CODENAME']
3444 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
3445 apt.write(PROPOSED_POCKET.format(release))
3446+ elif source == 'distro':
3447+ pass
3448 else:
3449- raise SourceConfigError("Unknown source: {!r}".format(source))
3450+ log("Unknown source: {!r}".format(source))
3451
3452 if key:
3453 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
3454- with NamedTemporaryFile() as key_file:
3455+ with NamedTemporaryFile('w+') as key_file:
3456 key_file.write(key)
3457 key_file.flush()
3458 key_file.seek(0)
3459@@ -292,14 +322,14 @@
3460 sources = safe_load((config(sources_var) or '').strip()) or []
3461 keys = safe_load((config(keys_var) or '').strip()) or None
3462
3463- if isinstance(sources, basestring):
3464+ if isinstance(sources, six.string_types):
3465 sources = [sources]
3466
3467 if keys is None:
3468 for source in sources:
3469 add_source(source, None)
3470 else:
3471- if isinstance(keys, basestring):
3472+ if isinstance(keys, six.string_types):
3473 keys = [keys]
3474
3475 if len(sources) != len(keys):
3476@@ -311,22 +341,35 @@
3477 apt_update(fatal=True)
3478
3479
3480-def install_remote(source):
3481+def install_remote(source, *args, **kwargs):
3482 """
3483 Install a file tree from a remote source
3484
3485 The specified source should be a url of the form:
3486 scheme://[host]/path[#[option=value][&...]]
3487
3488- Schemes supported are based on this modules submodules
3489- Options supported are submodule-specific"""
3490+ Schemes supported are based on this modules submodules.
3491+ Options supported are submodule-specific.
3492+ Additional arguments are passed through to the submodule.
3493+
3494+ For example::
3495+
3496+ dest = install_remote('http://example.com/archive.tgz',
3497+ checksum='deadbeef',
3498+ hash_type='sha1')
3499+
3500+ This will download `archive.tgz`, validate it using SHA1 and, if
3501+ the file is ok, extract it and return the directory in which it
3502+ was extracted. If the checksum fails, it will raise
3503+ :class:`charmhelpers.core.host.ChecksumError`.
3504+ """
3505 # We ONLY check for True here because can_handle may return a string
3506 # explaining why it can't handle a given source.
3507 handlers = [h for h in plugins() if h.can_handle(source) is True]
3508 installed_to = None
3509 for handler in handlers:
3510 try:
3511- installed_to = handler.install(source)
3512+ installed_to = handler.install(source, *args, **kwargs)
3513 except UnhandledSource:
3514 pass
3515 if not installed_to:
3516@@ -383,7 +426,7 @@
3517 while result is None or result == APT_NO_LOCK:
3518 try:
3519 result = subprocess.check_call(cmd, env=env)
3520- except subprocess.CalledProcessError, e:
3521+ except subprocess.CalledProcessError as e:
3522 retry_count = retry_count + 1
3523 if retry_count > APT_NO_LOCK_RETRY_COUNT:
3524 raise
3525
3526=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
3527--- hooks/charmhelpers/fetch/archiveurl.py 2014-07-16 05:40:55 +0000
3528+++ hooks/charmhelpers/fetch/archiveurl.py 2015-03-23 00:51:45 +0000
3529@@ -1,6 +1,22 @@
3530+# Copyright 2014-2015 Canonical Limited.
3531+#
3532+# This file is part of charm-helpers.
3533+#
3534+# charm-helpers is free software: you can redistribute it and/or modify
3535+# it under the terms of the GNU Lesser General Public License version 3 as
3536+# published by the Free Software Foundation.
3537+#
3538+# charm-helpers is distributed in the hope that it will be useful,
3539+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3540+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3541+# GNU Lesser General Public License for more details.
3542+#
3543+# You should have received a copy of the GNU Lesser General Public License
3544+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3545+
3546 import os
3547-import urllib2
3548-import urlparse
3549+import hashlib
3550+import re
3551
3552 from charmhelpers.fetch import (
3553 BaseFetchHandler,
3554@@ -10,11 +26,54 @@
3555 get_archive_handler,
3556 extract,
3557 )
3558-from charmhelpers.core.host import mkdir
3559+from charmhelpers.core.host import mkdir, check_hash
3560+
3561+import six
3562+if six.PY3:
3563+ from urllib.request import (
3564+ build_opener, install_opener, urlopen, urlretrieve,
3565+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
3566+ )
3567+ from urllib.parse import urlparse, urlunparse, parse_qs
3568+ from urllib.error import URLError
3569+else:
3570+ from urllib import urlretrieve
3571+ from urllib2 import (
3572+ build_opener, install_opener, urlopen,
3573+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
3574+ URLError
3575+ )
3576+ from urlparse import urlparse, urlunparse, parse_qs
3577+
3578+
3579+def splituser(host):
3580+ '''urllib.splituser(), but six's support of this seems broken'''
3581+ _userprog = re.compile('^(.*)@(.*)$')
3582+ match = _userprog.match(host)
3583+ if match:
3584+ return match.group(1, 2)
3585+ return None, host
3586+
3587+
3588+def splitpasswd(user):
3589+ '''urllib.splitpasswd(), but six's support of this is missing'''
3590+ _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
3591+ match = _passwdprog.match(user)
3592+ if match:
3593+ return match.group(1, 2)
3594+ return user, None
3595
3596
3597 class ArchiveUrlFetchHandler(BaseFetchHandler):
3598- """Handler for archives via generic URLs"""
3599+ """
3600+ Handler to download archive files from arbitrary URLs.
3601+
3602+ Can fetch from http, https, ftp, and file URLs.
3603+
3604+ Can install either tarballs (.tar, .tgz, .tbz2, etc) or zip files.
3605+
3606+ Installs the contents of the archive in $CHARM_DIR/fetched/.
3607+ """
3608 def can_handle(self, source):
3609 url_parts = self.parse_url(source)
3610 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
3611@@ -24,22 +83,28 @@
3612 return False
3613
3614 def download(self, source, dest):
3615+ """
3616+ Download an archive file.
3617+
3618+ :param str source: URL pointing to an archive file.
3619+ :param str dest: Local path location to download archive file to.
3620+ """
3621 # propogate all exceptions
3622 # URLError, OSError, etc
3623- proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
3624+ proto, netloc, path, params, query, fragment = urlparse(source)
3625 if proto in ('http', 'https'):
3626- auth, barehost = urllib2.splituser(netloc)
3627+ auth, barehost = splituser(netloc)
3628 if auth is not None:
3629- source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
3630- username, password = urllib2.splitpasswd(auth)
3631- passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
3632+ source = urlunparse((proto, barehost, path, params, query, fragment))
3633+ username, password = splitpasswd(auth)
3634+ passman = HTTPPasswordMgrWithDefaultRealm()
3635 # Realm is set to None in add_password to force the username and password
3636 # to be used whatever the realm
3637 passman.add_password(None, source, username, password)
3638- authhandler = urllib2.HTTPBasicAuthHandler(passman)
3639- opener = urllib2.build_opener(authhandler)
3640- urllib2.install_opener(opener)
3641- response = urllib2.urlopen(source)
3642+ authhandler = HTTPBasicAuthHandler(passman)
3643+ opener = build_opener(authhandler)
3644+ install_opener(opener)
3645+ response = urlopen(source)
3646 try:
3647 with open(dest, 'w') as dest_file:
3648 dest_file.write(response.read())
3649@@ -48,16 +113,49 @@
3650 os.unlink(dest)
3651 raise e
3652
3653- def install(self, source):
3654+ # Mandatory file validation via Sha1 or MD5 hashing.
3655+ def download_and_validate(self, url, hashsum, validate="sha1"):
3656+ tempfile, headers = urlretrieve(url)
3657+ check_hash(tempfile, hashsum, validate)
3658+ return tempfile
3659+
3660+ def install(self, source, dest=None, checksum=None, hash_type='sha1'):
3661+ """
3662+ Download and install an archive file, with optional checksum validation.
3663+
3664+ The checksum can also be given on the `source` URL's fragment.
3665+ For example::
3666+
3667+ handler.install('http://example.com/file.tgz#sha1=deadbeef')
3668+
3669+ :param str source: URL pointing to an archive file.
3670+ :param str dest: Local destination path to install to. If not given,
3671+ installs to `$CHARM_DIR/archives/archive_file_name`.
3672+ :param str checksum: If given, validate the archive file after download.
3673+ :param str hash_type: Algorithm used to generate `checksum`.
3674+ Can be any hash alrgorithm supported by :mod:`hashlib`,
3675+ such as md5, sha1, sha256, sha512, etc.
3676+
3677+ """
3678 url_parts = self.parse_url(source)
3679 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
3680 if not os.path.exists(dest_dir):
3681- mkdir(dest_dir, perms=0755)
3682+ mkdir(dest_dir, perms=0o755)
3683 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
3684 try:
3685 self.download(source, dld_file)
3686- except urllib2.URLError as e:
3687+ except URLError as e:
3688 raise UnhandledSource(e.reason)
3689 except OSError as e:
3690 raise UnhandledSource(e.strerror)
3691- return extract(dld_file)
3692+ options = parse_qs(url_parts.fragment)
3693+ for key, value in options.items():
3694+ if not six.PY3:
3695+ algorithms = hashlib.algorithms
3696+ else:
3697+ algorithms = hashlib.algorithms_available
3698+ if key in algorithms:
3699+ check_hash(dld_file, value, key)
3700+ if checksum:
3701+ check_hash(dld_file, checksum, hash_type)
3702+ return extract(dld_file, dest)
3703
3704=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
3705--- hooks/charmhelpers/fetch/bzrurl.py 2014-07-16 05:40:55 +0000
3706+++ hooks/charmhelpers/fetch/bzrurl.py 2015-03-23 00:51:45 +0000
3707@@ -1,3 +1,19 @@
3708+# Copyright 2014-2015 Canonical Limited.
3709+#
3710+# This file is part of charm-helpers.
3711+#
3712+# charm-helpers is free software: you can redistribute it and/or modify
3713+# it under the terms of the GNU Lesser General Public License version 3 as
3714+# published by the Free Software Foundation.
3715+#
3716+# charm-helpers is distributed in the hope that it will be useful,
3717+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3718+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3719+# GNU Lesser General Public License for more details.
3720+#
3721+# You should have received a copy of the GNU Lesser General Public License
3722+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3723+
3724 import os
3725 from charmhelpers.fetch import (
3726 BaseFetchHandler,
3727@@ -5,12 +21,18 @@
3728 )
3729 from charmhelpers.core.host import mkdir
3730
3731+import six
3732+if six.PY3:
3733+ raise ImportError('bzrlib does not support Python3')
3734+
3735 try:
3736 from bzrlib.branch import Branch
3737+ from bzrlib import bzrdir, workingtree, errors
3738 except ImportError:
3739 from charmhelpers.fetch import apt_install
3740 apt_install("python-bzrlib")
3741 from bzrlib.branch import Branch
3742+ from bzrlib import bzrdir, workingtree, errors
3743
3744
3745 class BzrUrlFetchHandler(BaseFetchHandler):
3746@@ -31,8 +53,14 @@
3747 from bzrlib.plugin import load_plugins
3748 load_plugins()
3749 try:
3750+ local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
3751+ except errors.AlreadyControlDirError:
3752+ local_branch = Branch.open(dest)
3753+ try:
3754 remote_branch = Branch.open(source)
3755- remote_branch.bzrdir.sprout(dest).open_branch()
3756+ remote_branch.push(local_branch)
3757+ tree = workingtree.WorkingTree.open(dest)
3758+ tree.update()
3759 except Exception as e:
3760 raise e
3761
3762@@ -42,7 +70,7 @@
3763 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3764 branch_name)
3765 if not os.path.exists(dest_dir):
3766- mkdir(dest_dir, perms=0755)
3767+ mkdir(dest_dir, perms=0o755)
3768 try:
3769 self.branch(source, dest_dir)
3770 except OSError as e:
3771
3772=== added file 'hooks/charmhelpers/fetch/giturl.py'
3773--- hooks/charmhelpers/fetch/giturl.py 1970-01-01 00:00:00 +0000
3774+++ hooks/charmhelpers/fetch/giturl.py 2015-03-23 00:51:45 +0000
3775@@ -0,0 +1,71 @@
3776+# Copyright 2014-2015 Canonical Limited.
3777+#
3778+# This file is part of charm-helpers.
3779+#
3780+# charm-helpers is free software: you can redistribute it and/or modify
3781+# it under the terms of the GNU Lesser General Public License version 3 as
3782+# published by the Free Software Foundation.
3783+#
3784+# charm-helpers is distributed in the hope that it will be useful,
3785+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3786+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3787+# GNU Lesser General Public License for more details.
3788+#
3789+# You should have received a copy of the GNU Lesser General Public License
3790+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3791+
3792+import os
3793+from charmhelpers.fetch import (
3794+ BaseFetchHandler,
3795+ UnhandledSource
3796+)
3797+from charmhelpers.core.host import mkdir
3798+
3799+import six
3800+if six.PY3:
3801+ raise ImportError('GitPython does not support Python 3')
3802+
3803+try:
3804+ from git import Repo
3805+except ImportError:
3806+ from charmhelpers.fetch import apt_install
3807+ apt_install("python-git")
3808+ from git import Repo
3809+
3810+from git.exc import GitCommandError # noqa E402
3811+
3812+
3813+class GitUrlFetchHandler(BaseFetchHandler):
3814+ """Handler for git branches via generic and github URLs"""
3815+ def can_handle(self, source):
3816+ url_parts = self.parse_url(source)
3817+ # TODO (mattyw) no support for ssh git@ yet
3818+ if url_parts.scheme not in ('http', 'https', 'git'):
3819+ return False
3820+ else:
3821+ return True
3822+
3823+ def clone(self, source, dest, branch):
3824+ if not self.can_handle(source):
3825+ raise UnhandledSource("Cannot handle {}".format(source))
3826+
3827+ repo = Repo.clone_from(source, dest)
3828+ repo.git.checkout(branch)
3829+
3830+ def install(self, source, branch="master", dest=None):
3831+ url_parts = self.parse_url(source)
3832+ branch_name = url_parts.path.strip("/").split("/")[-1]
3833+ if dest:
3834+ dest_dir = os.path.join(dest, branch_name)
3835+ else:
3836+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3837+ branch_name)
3838+ if not os.path.exists(dest_dir):
3839+ mkdir(dest_dir, perms=0o755)
3840+ try:
3841+ self.clone(source, dest_dir, branch)
3842+ except GitCommandError as e:
3843+ raise UnhandledSource(e.message)
3844+ except OSError as e:
3845+ raise UnhandledSource(e.strerror)
3846+ return dest_dir
3847
3848=== added symlink 'hooks/nrpe-external-master-relation-changed'
3849=== target is u'ntp_hooks.py'
3850=== added symlink 'hooks/nrpe-external-master-relation-joined'
3851=== target is u'ntp_hooks.py'
3852=== modified file 'hooks/ntp_hooks.py'
3853--- hooks/ntp_hooks.py 2014-08-05 05:12:15 +0000
3854+++ hooks/ntp_hooks.py 2015-03-23 00:51:45 +0000
3855@@ -9,6 +9,9 @@
3856 import shutil
3857 import os
3858
3859+from charmhelpers.contrib.charmsupport import nrpe
3860+
3861+NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
3862
3863 NTP_CONF = '/etc/ntp.conf'
3864 NTP_CONF_ORIG = '{}.orig'.format(NTP_CONF)
3865@@ -48,6 +51,42 @@
3866 ntpconf.write(render(os.path.basename(NTP_CONF),
3867 {'servers': remote_sources}))
3868
3869+ update_nrpe_config()
3870+
3871+
3872+@hooks.hook('nrpe-external-master-relation-joined',
3873+ 'nrpe-external-master-relation-changed')
3874+def update_nrpe_config():
3875+ # python-dbus is used by check_upstart_job
3876+ fetch.apt_install('python-dbus')
3877+ nagios_ntpmon_checks = hookenv.config('nagios_ntpmon_checks')
3878+ if os.path.isdir(NAGIOS_PLUGINS):
3879+ host.rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nagios',
3880+ 'check_ntpd.pl'),
3881+ os.path.join(NAGIOS_PLUGINS, 'check_ntpd.pl'))
3882+ if nagios_ntpmon_checks:
3883+ host.rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nagios',
3884+ 'check_ntpmon.py'),
3885+ os.path.join(NAGIOS_PLUGINS, 'check_ntpmon.py'))
3886+
3887+ hostname = nrpe.get_nagios_hostname()
3888+ current_unit = nrpe.get_nagios_unit_name()
3889+ nrpe_setup = nrpe.NRPE(hostname=hostname)
3890+ nrpe.add_init_service_checks(nrpe_setup, ['ntp'], current_unit)
3891+ nrpe_setup.add_check(
3892+ shortname="ntp_status",
3893+ description='Check NTP status {%s}' % current_unit,
3894+ check_cmd='check_ntpd.pl'
3895+ )
3896+ for nc in nagios_ntpmon_checks.split(" "):
3897+ nrpe_setup.add_check(
3898+ shortname="ntpmon_%s" % nc,
3899+ description='Check NTPmon %s {%s}' % (nc, current_unit),
3900+ check_cmd='check_ntpmon.py --check %s' % nc
3901+ )
3902+
3903+ nrpe_setup.write()
3904+
3905
3906 if __name__ == '__main__':
3907 try:
3908
3909=== modified file 'metadata.yaml'
3910--- metadata.yaml 2013-09-16 09:14:14 +0000
3911+++ metadata.yaml 2015-03-23 00:51:45 +0000
3912@@ -10,8 +10,12 @@
3913 .
3914 This charm can be deployed alongside principle charms to enable NTP
3915 management across deployed services.
3916-categories:
3917+tags:
3918 - misc
3919+provides:
3920+ nrpe-external-master:
3921+ interface: nrpe-external-master
3922+ scope: container
3923 requires:
3924 juju-info:
3925 interface: juju-info

Subscribers

People subscribed via source and target branches

to all changes: