Merge lp:~brad-marshall/charms/trusty/hacluster/add-nrpe-checks-update-charmhelpers into lp:charms/trusty/hacluster

Proposed by Brad Marshall
Status: Superseded
Proposed branch: lp:~brad-marshall/charms/trusty/hacluster/add-nrpe-checks-update-charmhelpers
Merge into: lp:charms/trusty/hacluster
Diff against target: 1667 lines (+1452/-14)
16 files modified
charm-helpers.yaml (+1/-0)
config.yaml (+10/-0)
files/nrpe/check_corosync_rings (+99/-0)
files/nrpe/check_crm (+201/-0)
files/nrpe/check_haproxy.sh (+32/-0)
files/nrpe/check_haproxy_queue_depth.sh (+30/-0)
files/sudoers/nagios (+5/-0)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+324/-0)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+175/-0)
hooks/charmhelpers/core/host.py (+5/-5)
hooks/charmhelpers/core/sysctl.py (+11/-5)
hooks/charmhelpers/core/templating.py (+3/-3)
hooks/charmhelpers/core/unitdata.py (+477/-0)
hooks/hooks.py (+61/-1)
metadata.yaml (+3/-0)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/hacluster/add-nrpe-checks-update-charmhelpers
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
charmers Pending
Review via email: mp+249448@code.launchpad.net

This proposal has been superseded by a proposal from 2015-02-12.

Description of the change

Adding nrpe checks to the hacluster to check the status of corosync.

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

[bradm] Removed unneeded import of relations_of_type

50. By Brad Marshall

[bradm] Removed haproxy nrpe checks

51. By Brad Marshall

[bradm] Sync charmhelpers

52. By Brad Marshall

[bradm] Add nagios_servicegroups config option

53. By Brad Marshall

[bradm] Handle case of empty nagios_servicegroups setting

54. By Brad Marshall

[bradm] Add process check for corosync and pacemakerd

55. By Brad Marshall

[bradm] Dropped back to just requiring charmhelpers.contrib.openstack.utils, don't need the rest now.

Unmerged revisions

55. By Brad Marshall

[bradm] Dropped back to just requiring charmhelpers.contrib.openstack.utils, don't need the rest now.

54. By Brad Marshall

[bradm] Add process check for corosync and pacemakerd

53. By Brad Marshall

[bradm] Handle case of empty nagios_servicegroups setting

52. By Brad Marshall

[bradm] Add nagios_servicegroups config option

51. By Brad Marshall

[bradm] Sync charmhelpers

50. By Brad Marshall

[bradm] Removed haproxy nrpe checks

49. By Brad Marshall

[bradm] Removed unneeded import of relations_of_type

48. By Brad Marshall

[bradm] Add sudoers files for nagios checks

47. By Brad Marshall

[bradm] Import nrpe at the right level to get to the functions required

46. By Brad Marshall

[bradm] Fixed order of nrpe variable

Preview Diff

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

Subscribers

People subscribed via source and target branches