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
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2015-01-26 10:44:46 +0000
+++ charm-helpers.yaml 2015-02-12 04:08:21 +0000
@@ -9,3 +9,4 @@
9 - contrib.network.ip9 - contrib.network.ip
10 - contrib.openstack.utils10 - contrib.openstack.utils
11 - contrib.python.packages11 - contrib.python.packages
12 - contrib.charmsupport
1213
=== modified file 'config.yaml'
--- config.yaml 2014-12-15 13:28:49 +0000
+++ config.yaml 2015-02-12 04:08:21 +0000
@@ -94,3 +94,13 @@
94 default: False94 default: False
95 type: boolean95 type: boolean
96 description: Enable debug logging96 description: Enable debug logging
97 nagios_context:
98 default: "juju"
99 type: string
100 description: |
101 Used by the nrpe-external-master subordinate charm.
102 A string that will be prepended to instance name to set the host name
103 in nagios. So for instance the hostname would be something like:
104 juju-postgresql-0
105 If you're running multiple environments with the same services in them
106 this allows you to differentiate between them.
97107
=== added directory 'files'
=== added directory 'files/nrpe'
=== added file 'files/nrpe/check_corosync_rings'
--- files/nrpe/check_corosync_rings 1970-01-01 00:00:00 +0000
+++ files/nrpe/check_corosync_rings 2015-02-12 04:08:21 +0000
@@ -0,0 +1,99 @@
1#!/usr/bin/perl
2#
3# check_corosync_rings
4#
5# Copyright © 2011 Phil Garner, Sysnix Consultants Limited
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20# Authors: Phil Garner - phil@sysnix.com & Peter Mottram peter@sysnix.com
21#
22# v0.1 05/01/2011
23# v0.2 31/10/2011 - additional crit when closing the file handle and additional
24# comments added
25#
26# NOTE:- Requires Perl 5.8 or higher & the Perl Module Nagios::Plugin
27# Nagios user will need sudo acces - suggest adding line below to
28# sudoers.
29# nagios ALL=(ALL) NOPASSWD: /usr/sbin/corosync-cfgtool -s
30#
31# In sudoers if requiretty is on (off state is default)
32# you will also need to add the line below
33# Defaults:nagios !requiretty
34#
35
36use warnings;
37use strict;
38use Nagios::Plugin;
39
40# Lines below may need changing if corosync-cfgtool or sudo installed in a
41# diffrent location.
42
43my $sudo = '/usr/bin/sudo';
44my $cfgtool = '/usr/sbin/corosync-cfgtool -s';
45
46# Now set up the plugin
47my $np = Nagios::Plugin->new(
48 shortname => 'check_cororings',
49 version => '0.2',
50 usage => "Usage: %s <ARGS>\n\t\t--help for help\n",
51 license => "License - GPL v3 see code for more details",
52 url => "http://www.sysnix.com",
53 blurb =>
54"\tNagios plugin that checks the status of corosync rings, requires Perl \t5.8+ and CPAN modules Nagios::Plugin.",
55);
56
57#Args
58$np->add_arg(
59 spec => 'rings|r=s',
60 help =>
61'How many rings should be running (optinal) sends Crit if incorrect number of rings found.',
62 required => 0,
63);
64
65$np->getopts;
66
67my $found = 0;
68my $fh;
69my $rings = $np->opts->rings;
70
71# Run cfgtools spin through output and get info needed
72
73open( $fh, "$sudo $cfgtool |" )
74 or $np->nagios_exit( CRITICAL, "Running corosync-cfgtool failed" );
75
76foreach my $line (<$fh>) {
77 if ( $line =~ m/status\s*=\s*(\S.+)/ ) {
78 my $status = $1;
79 if ( $status =~ m/^ring (\d+) active with no faults/ ) {
80 $np->add_message( OK, "ring $1 OK" );
81 }
82 else {
83 $np->add_message( CRITICAL, $status );
84 }
85 $found++;
86 }
87}
88
89close($fh) or $np->nagios_exit( CRITICAL, "Running corosync-cfgtool failed" );
90
91# Check we found some rings and apply -r arg if needed
92if ( $found == 0 ) {
93 $np->nagios_exit( CRITICAL, "No Rings Found" );
94}
95elsif ( defined $rings && $rings != $found ) {
96 $np->nagios_exit( CRITICAL, "Expected $rings rings but found $found" );
97}
98
99$np->nagios_exit( $np->check_messages() );
0100
=== added file 'files/nrpe/check_crm'
--- files/nrpe/check_crm 1970-01-01 00:00:00 +0000
+++ files/nrpe/check_crm 2015-02-12 04:08:21 +0000
@@ -0,0 +1,201 @@
1#!/usr/bin/perl
2#
3# check_crm_v0_7
4#
5# Copyright © 2013 Philip Garner, Sysnix Consultants Limited
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20# Authors: Phil Garner - phil@sysnix.com & Peter Mottram - peter@sysnix.com
21#
22# v0.1 09/01/2011
23# v0.2 11/01/2011
24# v0.3 22/08/2011 - bug fix and changes suggested by Vadym Chepkov
25# v0.4 23/08/2011 - update for spelling and anchor regex capture (Vadym Chepkov)
26# v0.5 29/09/2011 - Add standby warn/crit suggested by Sönke Martens & removal
27# of 'our' to 'my' to completely avoid problems with ePN
28# v0.6 14/03/2013 - Change from \w+ to \S+ in stopped check to cope with
29# Servers that have non word charachters in. Suggested by
30# Igal Baevsky.
31# v0.7 01/09/2013 - In testing as still not fully tested. Adds optional
32# constraints check (Boris Wesslowski). Adds fail count
33# threshold ( Zoran Bosnjak & Marko Hrastovec )
34#
35# NOTES: Requires Perl 5.8 or higher & the Perl Module Nagios::Plugin
36# Nagios user will need sudo acces - suggest adding line below to
37# sudoers
38# nagios ALL=(ALL) NOPASSWD: /usr/sbin/crm_mon -1 -r -f
39#
40# if you want to check for location constraints (-c) also add
41# nagios ALL=(ALL) NOPASSWD: /usr/sbin/crm configure show
42#
43# In sudoers if requiretty is on (off state is default)
44# you will also need to add the line below
45# Defaults:nagios !requiretty
46#
47
48use warnings;
49use strict;
50use Nagios::Plugin;
51
52# Lines below may need changing if crm_mon or sudo installed in a
53# different location.
54
55my $sudo = '/usr/bin/sudo';
56my $crm_mon = '/usr/sbin/crm_mon -1 -r -f';
57my $crm_configure_show = '/usr/sbin/crm configure show';
58
59my $np = Nagios::Plugin->new(
60 shortname => 'check_crm',
61 version => '0.7',
62 usage => "Usage: %s <ARGS>\n\t\t--help for help\n",
63);
64
65$np->add_arg(
66 spec => 'warning|w',
67 help =>
68'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',
69 required => 0,
70);
71
72$np->add_arg(
73 spec => 'standbyignore|s',
74 help => 'Ignore any node(s) in standby, by default sends Critical',
75 required => 0,
76);
77
78$np->add_arg(
79 spec => 'constraint|constraints|c',
80 help => 'Also check configuration for location constraints (caused by migrations) and warn if there are any. Requires additional privileges see notes',
81 required => 0,
82);
83
84$np->add_arg(
85 spec => 'failcount|failcounts|f=i',
86 help => 'resource fail count to start warning on [default = 1].',
87 required => 0,
88 default => 1,
89);
90
91$np->getopts;
92my $ConstraintsFlag = $np->opts->constraint;
93
94my @standby;
95
96# Check for -w option set warn if this is case instead of crit
97my $warn_or_crit = 'CRITICAL';
98$warn_or_crit = 'WARNING' if $np->opts->warning;
99
100my $fh;
101
102open( $fh, "$sudo $crm_mon |" )
103 or $np->nagios_exit( CRITICAL, "Running $sudo $crm_mon has failed" );
104
105foreach my $line (<$fh>) {
106
107 if ( $line =~ m/Connection to cluster failed\:(.*)/i ) {
108
109 # Check Cluster connected
110 $np->nagios_exit( CRITICAL, "Connection to cluster FAILED: $1" );
111 }
112 elsif ( $line =~ m/Current DC:/ ) {
113
114 # Check for Quorum
115 if ( $line =~ m/partition with quorum$/ ) {
116
117 # Assume cluster is OK - we only add warn/crit after here
118
119 $np->add_message( OK, "Cluster OK" );
120 }
121 else {
122 $np->add_message( CRITICAL, "No Quorum" );
123 }
124 }
125 elsif ( $line =~ m/^offline:\s*\[\s*(\S.*?)\s*\]/i ) {
126
127 # Count offline nodes
128 my @offline = split( /\s+/, $1 );
129 my $numoffline = scalar @offline;
130 $np->add_message( $warn_or_crit, ": $numoffline Nodes Offline" );
131 }
132 elsif ( $line =~ m/^node\s+(\S.*):\s*standby/i ) {
133
134 # Check for standby nodes (suggested by Sönke Martens)
135 # See later in code for message created from this
136 push @standby, $1;
137 }
138
139 elsif ( $line =~ m/\s*(\S+)\s+\(\S+\)\:\s+Stopped/ ) {
140
141 # Check Resources Stopped
142 $np->add_message( $warn_or_crit, ": $1 Stopped" );
143 }
144 elsif ( $line =~ m/\s*stopped\:\s*\[(.*)\]/i ) {
145
146 # Check Master/Slave stopped
147 $np->add_message( $warn_or_crit, ": $1 Stopped" );
148 }
149 elsif ( $line =~ m/^Failed actions\:/ ) {
150
151 # Check Failed Actions
152 $np->add_message( CRITICAL,
153 ": FAILED actions detected or not cleaned up" );
154 }
155 elsif ( $line =~ m/\s*(\S+?)\s+ \(.*\)\:\s+\w+\s+\w+\s+\(unmanaged\)\s+/i )
156 {
157
158 # Check Unmanaged
159 $np->add_message( CRITICAL, ": $1 unmanaged FAILED" );
160 }
161 elsif ( $line =~ m/\s*(\S+?)\s+ \(.*\)\:\s+not installed/i ) {
162
163 # Check for errors
164 $np->add_message( CRITICAL, ": $1 not installed" );
165 }
166 elsif ( $line =~ m/\s*(\S+?):.*fail-count=(\d+)/i ) {
167 if ( $2 >= $np->opts->failcount ) {
168
169 # Check for resource Fail count (suggested by Vadym Chepkov)
170 $np->add_message( WARNING, ": $1 failure detected, fail-count=$2" );
171 }
172 }
173}
174
175# If found any Nodes in standby & no -s option used send warn/crit
176if ( scalar @standby > 0 && !$np->opts->standbyignore ) {
177 $np->add_message( $warn_or_crit,
178 ": " . join( ', ', @standby ) . " in Standby" );
179}
180
181close($fh) or $np->nagios_exit( CRITICAL, "Running $crm_mon FAILED" );
182
183# if -c flag set check configuration for constraints
184if ($ConstraintsFlag) {
185
186 open( $fh, "$sudo $crm_configure_show|" )
187 or $np->nagios_exit( CRITICAL,
188 "Running $sudo $crm_configure_show has failed" );
189
190 foreach my $line (<$fh>) {
191 if ( $line =~ m/location cli-(prefer|standby)-\S+\s+(\S+)/ ) {
192 $np->add_message( WARNING,
193 ": $2 blocking location constraint detected" );
194 }
195 }
196 close($fh)
197 or $np->nagios_exit( CRITICAL, "Running $crm_configure_show FAILED" );
198}
199
200$np->nagios_exit( $np->check_messages() );
201
0202
=== added file 'files/nrpe/check_haproxy.sh'
--- files/nrpe/check_haproxy.sh 1970-01-01 00:00:00 +0000
+++ files/nrpe/check_haproxy.sh 2015-02-12 04:08:21 +0000
@@ -0,0 +1,32 @@
1#!/bin/bash
2#--------------------------------------------
3# This file is managed by Juju
4#--------------------------------------------
5#
6# Copyright 2009,2012 Canonical Ltd.
7# Author: Tom Haddon
8
9CRITICAL=0
10NOTACTIVE=''
11LOGFILE=/var/log/nagios/check_haproxy.log
12AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
13
14for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
15do
16 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')
17 if [ $? != 0 ]; then
18 date >> $LOGFILE
19 echo $output >> $LOGFILE
20 /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
21 CRITICAL=1
22 NOTACTIVE="${NOTACTIVE} $appserver"
23 fi
24done
25
26if [ $CRITICAL = 1 ]; then
27 echo "CRITICAL:${NOTACTIVE}"
28 exit 2
29fi
30
31echo "OK: All haproxy instances looking good"
32exit 0
033
=== added file 'files/nrpe/check_haproxy_queue_depth.sh'
--- files/nrpe/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
+++ files/nrpe/check_haproxy_queue_depth.sh 2015-02-12 04:08:21 +0000
@@ -0,0 +1,30 @@
1#!/bin/bash
2#--------------------------------------------
3# This file is managed by Juju
4#--------------------------------------------
5#
6# Copyright 2009,2012 Canonical Ltd.
7# Author: Tom Haddon
8
9# These should be config options at some stage
10CURRQthrsh=0
11MAXQthrsh=100
12
13AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
14
15HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
16
17for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
18do
19 CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
20 MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
21
22 if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
23 echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
24 exit 2
25 fi
26done
27
28echo "OK: All haproxy queue depths looking good"
29exit 0
30
031
=== added directory 'files/sudoers'
=== added file 'files/sudoers/nagios'
--- files/sudoers/nagios 1970-01-01 00:00:00 +0000
+++ files/sudoers/nagios 2015-02-12 04:08:21 +0000
@@ -0,0 +1,5 @@
1Defaults:nagios !requiretty
2
3nagios ALL=(ALL) NOPASSWD: /usr/sbin/corosync-cfgtool -s
4nagios ALL=(ALL) NOPASSWD: /usr/sbin/crm_mon -1 -r -f
5nagios ALL=(ALL) NOPASSWD: /usr/sbin/crm configure show
06
=== added directory 'hooks/charmhelpers/contrib/charmsupport'
=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
--- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-02-12 04:08:21 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-02-12 04:08:21 +0000
@@ -0,0 +1,324 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""Compatibility with the nrpe-external-master charm"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Matthew Wedgwood <matthew.wedgwood@canonical.com>
22
23import subprocess
24import pwd
25import grp
26import os
27import re
28import shlex
29import yaml
30
31from charmhelpers.core.hookenv import (
32 config,
33 local_unit,
34 log,
35 relation_ids,
36 relation_set,
37 relations_of_type,
38)
39
40from charmhelpers.core.host import service
41
42# This module adds compatibility with the nrpe-external-master and plain nrpe
43# subordinate charms. To use it in your charm:
44#
45# 1. Update metadata.yaml
46#
47# provides:
48# (...)
49# nrpe-external-master:
50# interface: nrpe-external-master
51# scope: container
52#
53# and/or
54#
55# provides:
56# (...)
57# local-monitors:
58# interface: local-monitors
59# scope: container
60
61#
62# 2. Add the following to config.yaml
63#
64# nagios_context:
65# default: "juju"
66# type: string
67# description: |
68# Used by the nrpe subordinate charms.
69# A string that will be prepended to instance name to set the host name
70# in nagios. So for instance the hostname would be something like:
71# juju-myservice-0
72# If you're running multiple environments with the same services in them
73# this allows you to differentiate between them.
74# nagios_servicegroups:
75# default: ""
76# type: string
77# description: |
78# A comma-separated list of nagios servicegroups.
79# If left empty, the nagios_context will be used as the servicegroup
80#
81# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
82#
83# 4. Update your hooks.py with something like this:
84#
85# from charmsupport.nrpe import NRPE
86# (...)
87# def update_nrpe_config():
88# nrpe_compat = NRPE()
89# nrpe_compat.add_check(
90# shortname = "myservice",
91# description = "Check MyService",
92# check_cmd = "check_http -w 2 -c 10 http://localhost"
93# )
94# nrpe_compat.add_check(
95# "myservice_other",
96# "Check for widget failures",
97# check_cmd = "/srv/myapp/scripts/widget_check"
98# )
99# nrpe_compat.write()
100#
101# def config_changed():
102# (...)
103# update_nrpe_config()
104#
105# def nrpe_external_master_relation_changed():
106# update_nrpe_config()
107#
108# def local_monitors_relation_changed():
109# update_nrpe_config()
110#
111# 5. ln -s hooks.py nrpe-external-master-relation-changed
112# ln -s hooks.py local-monitors-relation-changed
113
114
115class CheckException(Exception):
116 pass
117
118
119class Check(object):
120 shortname_re = '[A-Za-z0-9-_]+$'
121 service_template = ("""
122#---------------------------------------------------
123# This file is Juju managed
124#---------------------------------------------------
125define service {{
126 use active-service
127 host_name {nagios_hostname}
128 service_description {nagios_hostname}[{shortname}] """
129 """{description}
130 check_command check_nrpe!{command}
131 servicegroups {nagios_servicegroup}
132}}
133""")
134
135 def __init__(self, shortname, description, check_cmd):
136 super(Check, self).__init__()
137 # XXX: could be better to calculate this from the service name
138 if not re.match(self.shortname_re, shortname):
139 raise CheckException("shortname must match {}".format(
140 Check.shortname_re))
141 self.shortname = shortname
142 self.command = "check_{}".format(shortname)
143 # Note: a set of invalid characters is defined by the
144 # Nagios server config
145 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
146 self.description = description
147 self.check_cmd = self._locate_cmd(check_cmd)
148
149 def _locate_cmd(self, check_cmd):
150 search_path = (
151 '/usr/lib/nagios/plugins',
152 '/usr/local/lib/nagios/plugins',
153 )
154 parts = shlex.split(check_cmd)
155 for path in search_path:
156 if os.path.exists(os.path.join(path, parts[0])):
157 command = os.path.join(path, parts[0])
158 if len(parts) > 1:
159 command += " " + " ".join(parts[1:])
160 return command
161 log('Check command not found: {}'.format(parts[0]))
162 return ''
163
164 def write(self, nagios_context, hostname, nagios_servicegroups=None):
165 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
166 self.command)
167 with open(nrpe_check_file, 'w') as nrpe_check_config:
168 nrpe_check_config.write("# check {}\n".format(self.shortname))
169 nrpe_check_config.write("command[{}]={}\n".format(
170 self.command, self.check_cmd))
171
172 if not os.path.exists(NRPE.nagios_exportdir):
173 log('Not writing service config as {} is not accessible'.format(
174 NRPE.nagios_exportdir))
175 else:
176 self.write_service_config(nagios_context, hostname,
177 nagios_servicegroups)
178
179 def write_service_config(self, nagios_context, hostname,
180 nagios_servicegroups=None):
181 for f in os.listdir(NRPE.nagios_exportdir):
182 if re.search('.*{}.cfg'.format(self.command), f):
183 os.remove(os.path.join(NRPE.nagios_exportdir, f))
184
185 if not nagios_servicegroups:
186 nagios_servicegroups = nagios_context
187
188 templ_vars = {
189 'nagios_hostname': hostname,
190 'nagios_servicegroup': nagios_servicegroups,
191 'description': self.description,
192 'shortname': self.shortname,
193 'command': self.command,
194 }
195 nrpe_service_text = Check.service_template.format(**templ_vars)
196 nrpe_service_file = '{}/service__{}_{}.cfg'.format(
197 NRPE.nagios_exportdir, hostname, self.command)
198 with open(nrpe_service_file, 'w') as nrpe_service_config:
199 nrpe_service_config.write(str(nrpe_service_text))
200
201 def run(self):
202 subprocess.call(self.check_cmd)
203
204
205class NRPE(object):
206 nagios_logdir = '/var/log/nagios'
207 nagios_exportdir = '/var/lib/nagios/export'
208 nrpe_confdir = '/etc/nagios/nrpe.d'
209
210 def __init__(self, hostname=None):
211 super(NRPE, self).__init__()
212 self.config = config()
213 self.nagios_context = self.config['nagios_context']
214 if 'nagios_servicegroups' in self.config:
215 self.nagios_servicegroups = self.config['nagios_servicegroups']
216 else:
217 self.nagios_servicegroups = 'juju'
218 self.unit_name = local_unit().replace('/', '-')
219 if hostname:
220 self.hostname = hostname
221 else:
222 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
223 self.checks = []
224
225 def add_check(self, *args, **kwargs):
226 self.checks.append(Check(*args, **kwargs))
227
228 def write(self):
229 try:
230 nagios_uid = pwd.getpwnam('nagios').pw_uid
231 nagios_gid = grp.getgrnam('nagios').gr_gid
232 except:
233 log("Nagios user not set up, nrpe checks not updated")
234 return
235
236 if not os.path.exists(NRPE.nagios_logdir):
237 os.mkdir(NRPE.nagios_logdir)
238 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
239
240 nrpe_monitors = {}
241 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
242 for nrpecheck in self.checks:
243 nrpecheck.write(self.nagios_context, self.hostname,
244 self.nagios_servicegroups)
245 nrpe_monitors[nrpecheck.shortname] = {
246 "command": nrpecheck.command,
247 }
248
249 service('restart', 'nagios-nrpe-server')
250
251 for rid in relation_ids("local-monitors"):
252 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
253
254
255def get_nagios_hostcontext(relation_name='nrpe-external-master'):
256 """
257 Query relation with nrpe subordinate, return the nagios_host_context
258
259 :param str relation_name: Name of relation nrpe sub joined to
260 """
261 for rel in relations_of_type(relation_name):
262 if 'nagios_hostname' in rel:
263 return rel['nagios_host_context']
264
265
266def get_nagios_hostname(relation_name='nrpe-external-master'):
267 """
268 Query relation with nrpe subordinate, return the nagios_hostname
269
270 :param str relation_name: Name of relation nrpe sub joined to
271 """
272 for rel in relations_of_type(relation_name):
273 if 'nagios_hostname' in rel:
274 return rel['nagios_hostname']
275
276
277def get_nagios_unit_name(relation_name='nrpe-external-master'):
278 """
279 Return the nagios unit name prepended with host_context if needed
280
281 :param str relation_name: Name of relation nrpe sub joined to
282 """
283 host_context = get_nagios_hostcontext(relation_name)
284 if host_context:
285 unit = "%s:%s" % (host_context, local_unit())
286 else:
287 unit = local_unit()
288 return unit
289
290
291def add_init_service_checks(nrpe, services, unit_name):
292 """
293 Add checks for each service in list
294
295 :param NRPE nrpe: NRPE object to add check to
296 :param list services: List of services to check
297 :param str unit_name: Unit name to use in check description
298 """
299 for svc in services:
300 upstart_init = '/etc/init/%s.conf' % svc
301 sysv_init = '/etc/init.d/%s' % svc
302 if os.path.exists(upstart_init):
303 nrpe.add_check(
304 shortname=svc,
305 description='process check {%s}' % unit_name,
306 check_cmd='check_upstart_job %s' % svc
307 )
308 elif os.path.exists(sysv_init):
309 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
310 cron_file = ('*/5 * * * * root '
311 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
312 '-s /etc/init.d/%s status > '
313 '/var/lib/nagios/service-check-%s.txt\n' % (svc,
314 svc)
315 )
316 f = open(cronpath, 'w')
317 f.write(cron_file)
318 f.close()
319 nrpe.add_check(
320 shortname=svc,
321 description='process check {%s}' % unit_name,
322 check_cmd='check_status_file.py -f '
323 '/var/lib/nagios/service-check-%s.txt' % svc,
324 )
0325
=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-02-12 04:08:21 +0000
@@ -0,0 +1,175 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17'''
18Functions for managing volumes in juju units. One volume is supported per unit.
19Subordinates may have their own storage, provided it is on its own partition.
20
21Configuration stanzas::
22
23 volume-ephemeral:
24 type: boolean
25 default: true
26 description: >
27 If false, a volume is mounted as sepecified in "volume-map"
28 If true, ephemeral storage will be used, meaning that log data
29 will only exist as long as the machine. YOU HAVE BEEN WARNED.
30 volume-map:
31 type: string
32 default: {}
33 description: >
34 YAML map of units to device names, e.g:
35 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36 Service units will raise a configure-error if volume-ephemeral
37 is 'true' and no volume-map value is set. Use 'juju set' to set a
38 value and 'juju resolved' to complete configuration.
39
40Usage::
41
42 from charmsupport.volumes import configure_volume, VolumeConfigurationError
43 from charmsupport.hookenv import log, ERROR
44 def post_mount_hook():
45 stop_service('myservice')
46 def post_mount_hook():
47 start_service('myservice')
48
49 if __name__ == '__main__':
50 try:
51 configure_volume(before_change=pre_mount_hook,
52 after_change=post_mount_hook)
53 except VolumeConfigurationError:
54 log('Storage could not be configured', ERROR)
55
56'''
57
58# XXX: Known limitations
59# - fstab is neither consulted nor updated
60
61import os
62from charmhelpers.core import hookenv
63from charmhelpers.core import host
64import yaml
65
66
67MOUNT_BASE = '/srv/juju/volumes'
68
69
70class VolumeConfigurationError(Exception):
71 '''Volume configuration data is missing or invalid'''
72 pass
73
74
75def get_config():
76 '''Gather and sanity-check volume configuration data'''
77 volume_config = {}
78 config = hookenv.config()
79
80 errors = False
81
82 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83 volume_config['ephemeral'] = True
84 else:
85 volume_config['ephemeral'] = False
86
87 try:
88 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89 except yaml.YAMLError as e:
90 hookenv.log("Error parsing YAML volume-map: {}".format(e),
91 hookenv.ERROR)
92 errors = True
93 if volume_map is None:
94 # probably an empty string
95 volume_map = {}
96 elif not isinstance(volume_map, dict):
97 hookenv.log("Volume-map should be a dictionary, not {}".format(
98 type(volume_map)))
99 errors = True
100
101 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102 if volume_config['device'] and volume_config['ephemeral']:
103 # asked for ephemeral storage but also defined a volume ID
104 hookenv.log('A volume is defined for this unit, but ephemeral '
105 'storage was requested', hookenv.ERROR)
106 errors = True
107 elif not volume_config['device'] and not volume_config['ephemeral']:
108 # asked for permanent storage but did not define volume ID
109 hookenv.log('Ephemeral storage was requested, but there is no volume '
110 'defined for this unit.', hookenv.ERROR)
111 errors = True
112
113 unit_mount_name = hookenv.local_unit().replace('/', '-')
114 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
115
116 if errors:
117 return None
118 return volume_config
119
120
121def mount_volume(config):
122 if os.path.exists(config['mountpoint']):
123 if not os.path.isdir(config['mountpoint']):
124 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125 raise VolumeConfigurationError()
126 else:
127 host.mkdir(config['mountpoint'])
128 if os.path.ismount(config['mountpoint']):
129 unmount_volume(config)
130 if not host.mount(config['device'], config['mountpoint'], persist=True):
131 raise VolumeConfigurationError()
132
133
134def unmount_volume(config):
135 if os.path.ismount(config['mountpoint']):
136 if not host.umount(config['mountpoint'], persist=True):
137 raise VolumeConfigurationError()
138
139
140def managed_mounts():
141 '''List of all mounted managed volumes'''
142 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
143
144
145def configure_volume(before_change=lambda: None, after_change=lambda: None):
146 '''Set up storage (or don't) according to the charm's volume configuration.
147 Returns the mount point or "ephemeral". before_change and after_change
148 are optional functions to be called if the volume configuration changes.
149 '''
150
151 config = get_config()
152 if not config:
153 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154 raise VolumeConfigurationError()
155
156 if config['ephemeral']:
157 if os.path.ismount(config['mountpoint']):
158 before_change()
159 unmount_volume(config)
160 after_change()
161 return 'ephemeral'
162 else:
163 # persistent storage
164 if os.path.ismount(config['mountpoint']):
165 mounts = dict(managed_mounts())
166 if mounts.get(config['mountpoint']) != config['device']:
167 before_change()
168 unmount_volume(config)
169 mount_volume(config)
170 after_change()
171 else:
172 before_change()
173 mount_volume(config)
174 after_change()
175 return config['mountpoint']
0176
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-01-26 10:44:46 +0000
+++ hooks/charmhelpers/core/host.py 2015-02-12 04:08:21 +0000
@@ -191,11 +191,11 @@
191191
192192
193def write_file(path, content, owner='root', group='root', perms=0o444):193def write_file(path, content, owner='root', group='root', perms=0o444):
194 """Create or overwrite a file with the contents of a string"""194 """Create or overwrite a file with the contents of a byte string."""
195 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))195 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
196 uid = pwd.getpwnam(owner).pw_uid196 uid = pwd.getpwnam(owner).pw_uid
197 gid = grp.getgrnam(group).gr_gid197 gid = grp.getgrnam(group).gr_gid
198 with open(path, 'w') as target:198 with open(path, 'wb') as target:
199 os.fchown(target.fileno(), uid, gid)199 os.fchown(target.fileno(), uid, gid)
200 os.fchmod(target.fileno(), perms)200 os.fchmod(target.fileno(), perms)
201 target.write(content)201 target.write(content)
@@ -305,11 +305,11 @@
305 ceph_client_changed function.305 ceph_client_changed function.
306 """306 """
307 def wrap(f):307 def wrap(f):
308 def wrapped_f(*args):308 def wrapped_f(*args, **kwargs):
309 checksums = {}309 checksums = {}
310 for path in restart_map:310 for path in restart_map:
311 checksums[path] = file_hash(path)311 checksums[path] = file_hash(path)
312 f(*args)312 f(*args, **kwargs)
313 restarts = []313 restarts = []
314 for path in restart_map:314 for path in restart_map:
315 if checksums[path] != file_hash(path):315 if checksums[path] != file_hash(path):
@@ -361,7 +361,7 @@
361 ip_output = (line for line in ip_output if line)361 ip_output = (line for line in ip_output if line)
362 for line in ip_output:362 for line in ip_output:
363 if line.split()[1].startswith(int_type):363 if line.split()[1].startswith(int_type):
364 matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)364 matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
365 if matched:365 if matched:
366 interface = matched.groups()[0]366 interface = matched.groups()[0]
367 else:367 else:
368368
=== modified file 'hooks/charmhelpers/core/sysctl.py'
--- hooks/charmhelpers/core/sysctl.py 2015-01-26 10:44:46 +0000
+++ hooks/charmhelpers/core/sysctl.py 2015-02-12 04:08:21 +0000
@@ -26,25 +26,31 @@
26from charmhelpers.core.hookenv import (26from charmhelpers.core.hookenv import (
27 log,27 log,
28 DEBUG,28 DEBUG,
29 ERROR,
29)30)
3031
3132
32def create(sysctl_dict, sysctl_file):33def create(sysctl_dict, sysctl_file):
33 """Creates a sysctl.conf file from a YAML associative array34 """Creates a sysctl.conf file from a YAML associative array
3435
35 :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }36 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
36 :type sysctl_dict: dict37 :type sysctl_dict: str
37 :param sysctl_file: path to the sysctl file to be saved38 :param sysctl_file: path to the sysctl file to be saved
38 :type sysctl_file: str or unicode39 :type sysctl_file: str or unicode
39 :returns: None40 :returns: None
40 """41 """
41 sysctl_dict = yaml.load(sysctl_dict)42 try:
43 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
44 except yaml.YAMLError:
45 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
46 level=ERROR)
47 return
4248
43 with open(sysctl_file, "w") as fd:49 with open(sysctl_file, "w") as fd:
44 for key, value in sysctl_dict.items():50 for key, value in sysctl_dict_parsed.items():
45 fd.write("{}={}\n".format(key, value))51 fd.write("{}={}\n".format(key, value))
4652
47 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),53 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
48 level=DEBUG)54 level=DEBUG)
4955
50 check_call(["sysctl", "-p", sysctl_file])56 check_call(["sysctl", "-p", sysctl_file])
5157
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2015-01-26 10:44:46 +0000
+++ hooks/charmhelpers/core/templating.py 2015-02-12 04:08:21 +0000
@@ -21,7 +21,7 @@
2121
2222
23def render(source, target, context, owner='root', group='root',23def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None):24 perms=0o444, templates_dir=None, encoding='UTF-8'):
25 """25 """
26 Render a template.26 Render a template.
2727
@@ -64,5 +64,5 @@
64 level=hookenv.ERROR)64 level=hookenv.ERROR)
65 raise e65 raise e
66 content = template.render(context)66 content = template.render(context)
67 host.mkdir(os.path.dirname(target), owner, group)67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
68 host.write_file(target, content, owner, group, perms)68 host.write_file(target, content.encode(encoding), owner, group, perms)
6969
=== added file 'hooks/charmhelpers/core/unitdata.py'
--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/unitdata.py 2015-02-12 04:08:21 +0000
@@ -0,0 +1,477 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19#
20#
21# Authors:
22# Kapil Thangavelu <kapil.foss@gmail.com>
23#
24"""
25Intro
26-----
27
28A simple way to store state in units. This provides a key value
29storage with support for versioned, transactional operation,
30and can calculate deltas from previous values to simplify unit logic
31when processing changes.
32
33
34Hook Integration
35----------------
36
37There are several extant frameworks for hook execution, including
38
39 - charmhelpers.core.hookenv.Hooks
40 - charmhelpers.core.services.ServiceManager
41
42The storage classes are framework agnostic, one simple integration is
43via the HookData contextmanager. It will record the current hook
44execution environment (including relation data, config data, etc.),
45setup a transaction and allow easy access to the changes from
46previously seen values. One consequence of the integration is the
47reservation of particular keys ('rels', 'unit', 'env', 'config',
48'charm_revisions') for their respective values.
49
50Here's a fully worked integration example using hookenv.Hooks::
51
52 from charmhelper.core import hookenv, unitdata
53
54 hook_data = unitdata.HookData()
55 db = unitdata.kv()
56 hooks = hookenv.Hooks()
57
58 @hooks.hook
59 def config_changed():
60 # Print all changes to configuration from previously seen
61 # values.
62 for changed, (prev, cur) in hook_data.conf.items():
63 print('config changed', changed,
64 'previous value', prev,
65 'current value', cur)
66
67 # Get some unit specific bookeeping
68 if not db.get('pkg_key'):
69 key = urllib.urlopen('https://example.com/pkg_key').read()
70 db.set('pkg_key', key)
71
72 # Directly access all charm config as a mapping.
73 conf = db.getrange('config', True)
74
75 # Directly access all relation data as a mapping
76 rels = db.getrange('rels', True)
77
78 if __name__ == '__main__':
79 with hook_data():
80 hook.execute()
81
82
83A more basic integration is via the hook_scope context manager which simply
84manages transaction scope (and records hook name, and timestamp)::
85
86 >>> from unitdata import kv
87 >>> db = kv()
88 >>> with db.hook_scope('install'):
89 ... # do work, in transactional scope.
90 ... db.set('x', 1)
91 >>> db.get('x')
92 1
93
94
95Usage
96-----
97
98Values are automatically json de/serialized to preserve basic typing
99and complex data struct capabilities (dicts, lists, ints, booleans, etc).
100
101Individual values can be manipulated via get/set::
102
103 >>> kv.set('y', True)
104 >>> kv.get('y')
105 True
106
107 # We can set complex values (dicts, lists) as a single key.
108 >>> kv.set('config', {'a': 1, 'b': True'})
109
110 # Also supports returning dictionaries as a record which
111 # provides attribute access.
112 >>> config = kv.get('config', record=True)
113 >>> config.b
114 True
115
116
117Groups of keys can be manipulated with update/getrange::
118
119 >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
120 >>> kv.getrange('gui.', strip=True)
121 {'z': 1, 'y': 2}
122
123When updating values, its very helpful to understand which values
124have actually changed and how have they changed. The storage
125provides a delta method to provide for this::
126
127 >>> data = {'debug': True, 'option': 2}
128 >>> delta = kv.delta(data, 'config.')
129 >>> delta.debug.previous
130 None
131 >>> delta.debug.current
132 True
133 >>> delta
134 {'debug': (None, True), 'option': (None, 2)}
135
136Note the delta method does not persist the actual change, it needs to
137be explicitly saved via 'update' method::
138
139 >>> kv.update(data, 'config.')
140
141Values modified in the context of a hook scope retain historical values
142associated to the hookname.
143
144 >>> with db.hook_scope('config-changed'):
145 ... db.set('x', 42)
146 >>> db.gethistory('x')
147 [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
148 (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
149
150"""
151
152import collections
153import contextlib
154import datetime
155import json
156import os
157import pprint
158import sqlite3
159import sys
160
161__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
162
163
164class Storage(object):
165 """Simple key value database for local unit state within charms.
166
167 Modifications are automatically committed at hook exit. That's
168 currently regardless of exit code.
169
170 To support dicts, lists, integer, floats, and booleans values
171 are automatically json encoded/decoded.
172 """
173 def __init__(self, path=None):
174 self.db_path = path
175 if path is None:
176 self.db_path = os.path.join(
177 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
178 self.conn = sqlite3.connect('%s' % self.db_path)
179 self.cursor = self.conn.cursor()
180 self.revision = None
181 self._closed = False
182 self._init()
183
184 def close(self):
185 if self._closed:
186 return
187 self.flush(False)
188 self.cursor.close()
189 self.conn.close()
190 self._closed = True
191
192 def _scoped_query(self, stmt, params=None):
193 if params is None:
194 params = []
195 return stmt, params
196
197 def get(self, key, default=None, record=False):
198 self.cursor.execute(
199 *self._scoped_query(
200 'select data from kv where key=?', [key]))
201 result = self.cursor.fetchone()
202 if not result:
203 return default
204 if record:
205 return Record(json.loads(result[0]))
206 return json.loads(result[0])
207
208 def getrange(self, key_prefix, strip=False):
209 stmt = "select key, data from kv where key like '%s%%'" % key_prefix
210 self.cursor.execute(*self._scoped_query(stmt))
211 result = self.cursor.fetchall()
212
213 if not result:
214 return None
215 if not strip:
216 key_prefix = ''
217 return dict([
218 (k[len(key_prefix):], json.loads(v)) for k, v in result])
219
220 def update(self, mapping, prefix=""):
221 for k, v in mapping.items():
222 self.set("%s%s" % (prefix, k), v)
223
224 def unset(self, key):
225 self.cursor.execute('delete from kv where key=?', [key])
226 if self.revision and self.cursor.rowcount:
227 self.cursor.execute(
228 'insert into kv_revisions values (?, ?, ?)',
229 [key, self.revision, json.dumps('DELETED')])
230
231 def set(self, key, value):
232 serialized = json.dumps(value)
233
234 self.cursor.execute(
235 'select data from kv where key=?', [key])
236 exists = self.cursor.fetchone()
237
238 # Skip mutations to the same value
239 if exists:
240 if exists[0] == serialized:
241 return value
242
243 if not exists:
244 self.cursor.execute(
245 'insert into kv (key, data) values (?, ?)',
246 (key, serialized))
247 else:
248 self.cursor.execute('''
249 update kv
250 set data = ?
251 where key = ?''', [serialized, key])
252
253 # Save
254 if not self.revision:
255 return value
256
257 self.cursor.execute(
258 'select 1 from kv_revisions where key=? and revision=?',
259 [key, self.revision])
260 exists = self.cursor.fetchone()
261
262 if not exists:
263 self.cursor.execute(
264 '''insert into kv_revisions (
265 revision, key, data) values (?, ?, ?)''',
266 (self.revision, key, serialized))
267 else:
268 self.cursor.execute(
269 '''
270 update kv_revisions
271 set data = ?
272 where key = ?
273 and revision = ?''',
274 [serialized, key, self.revision])
275
276 return value
277
278 def delta(self, mapping, prefix):
279 """
280 return a delta containing values that have changed.
281 """
282 previous = self.getrange(prefix, strip=True)
283 if not previous:
284 pk = set()
285 else:
286 pk = set(previous.keys())
287 ck = set(mapping.keys())
288 delta = DeltaSet()
289
290 # added
291 for k in ck.difference(pk):
292 delta[k] = Delta(None, mapping[k])
293
294 # removed
295 for k in pk.difference(ck):
296 delta[k] = Delta(previous[k], None)
297
298 # changed
299 for k in pk.intersection(ck):
300 c = mapping[k]
301 p = previous[k]
302 if c != p:
303 delta[k] = Delta(p, c)
304
305 return delta
306
307 @contextlib.contextmanager
308 def hook_scope(self, name=""):
309 """Scope all future interactions to the current hook execution
310 revision."""
311 assert not self.revision
312 self.cursor.execute(
313 'insert into hooks (hook, date) values (?, ?)',
314 (name or sys.argv[0],
315 datetime.datetime.utcnow().isoformat()))
316 self.revision = self.cursor.lastrowid
317 try:
318 yield self.revision
319 self.revision = None
320 except:
321 self.flush(False)
322 self.revision = None
323 raise
324 else:
325 self.flush()
326
327 def flush(self, save=True):
328 if save:
329 self.conn.commit()
330 elif self._closed:
331 return
332 else:
333 self.conn.rollback()
334
335 def _init(self):
336 self.cursor.execute('''
337 create table if not exists kv (
338 key text,
339 data text,
340 primary key (key)
341 )''')
342 self.cursor.execute('''
343 create table if not exists kv_revisions (
344 key text,
345 revision integer,
346 data text,
347 primary key (key, revision)
348 )''')
349 self.cursor.execute('''
350 create table if not exists hooks (
351 version integer primary key autoincrement,
352 hook text,
353 date text
354 )''')
355 self.conn.commit()
356
357 def gethistory(self, key, deserialize=False):
358 self.cursor.execute(
359 '''
360 select kv.revision, kv.key, kv.data, h.hook, h.date
361 from kv_revisions kv,
362 hooks h
363 where kv.key=?
364 and kv.revision = h.version
365 ''', [key])
366 if deserialize is False:
367 return self.cursor.fetchall()
368 return map(_parse_history, self.cursor.fetchall())
369
370 def debug(self, fh=sys.stderr):
371 self.cursor.execute('select * from kv')
372 pprint.pprint(self.cursor.fetchall(), stream=fh)
373 self.cursor.execute('select * from kv_revisions')
374 pprint.pprint(self.cursor.fetchall(), stream=fh)
375
376
377def _parse_history(d):
378 return (d[0], d[1], json.loads(d[2]), d[3],
379 datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
380
381
382class HookData(object):
383 """Simple integration for existing hook exec frameworks.
384
385 Records all unit information, and stores deltas for processing
386 by the hook.
387
388 Sample::
389
390 from charmhelper.core import hookenv, unitdata
391
392 changes = unitdata.HookData()
393 db = unitdata.kv()
394 hooks = hookenv.Hooks()
395
396 @hooks.hook
397 def config_changed():
398 # View all changes to configuration
399 for changed, (prev, cur) in changes.conf.items():
400 print('config changed', changed,
401 'previous value', prev,
402 'current value', cur)
403
404 # Get some unit specific bookeeping
405 if not db.get('pkg_key'):
406 key = urllib.urlopen('https://example.com/pkg_key').read()
407 db.set('pkg_key', key)
408
409 if __name__ == '__main__':
410 with changes():
411 hook.execute()
412
413 """
414 def __init__(self):
415 self.kv = kv()
416 self.conf = None
417 self.rels = None
418
419 @contextlib.contextmanager
420 def __call__(self):
421 from charmhelpers.core import hookenv
422 hook_name = hookenv.hook_name()
423
424 with self.kv.hook_scope(hook_name):
425 self._record_charm_version(hookenv.charm_dir())
426 delta_config, delta_relation = self._record_hook(hookenv)
427 yield self.kv, delta_config, delta_relation
428
429 def _record_charm_version(self, charm_dir):
430 # Record revisions.. charm revisions are meaningless
431 # to charm authors as they don't control the revision.
432 # so logic dependnent on revision is not particularly
433 # useful, however it is useful for debugging analysis.
434 charm_rev = open(
435 os.path.join(charm_dir, 'revision')).read().strip()
436 charm_rev = charm_rev or '0'
437 revs = self.kv.get('charm_revisions', [])
438 if not charm_rev in revs:
439 revs.append(charm_rev.strip() or '0')
440 self.kv.set('charm_revisions', revs)
441
442 def _record_hook(self, hookenv):
443 data = hookenv.execution_environment()
444 self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
445 self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
446 self.kv.set('env', data['env'])
447 self.kv.set('unit', data['unit'])
448 self.kv.set('relid', data.get('relid'))
449 return conf_delta, rels_delta
450
451
452class Record(dict):
453
454 __slots__ = ()
455
456 def __getattr__(self, k):
457 if k in self:
458 return self[k]
459 raise AttributeError(k)
460
461
462class DeltaSet(Record):
463
464 __slots__ = ()
465
466
467Delta = collections.namedtuple('Delta', ['previous', 'current'])
468
469
470_KV = None
471
472
473def kv():
474 global _KV
475 if _KV is None:
476 _KV = Storage()
477 return _KV
0478
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2014-12-17 17:17:29 +0000
+++ hooks/hooks.py 2015-02-12 04:08:21 +0000
@@ -11,6 +11,7 @@
11import shutil11import shutil
12import sys12import sys
13import os13import os
14import glob
14from base64 import b64decode15from base64 import b64decode
1516
16import maas as MAAS17import maas as MAAS
@@ -24,6 +25,7 @@
24 related_units,25 related_units,
25 relation_ids,26 relation_ids,
26 relation_set,27 relation_set,
28 relations_of_type,
27 unit_get,29 unit_get,
28 config,30 config,
29 Hooks, UnregisteredHookError,31 Hooks, UnregisteredHookError,
@@ -56,6 +58,8 @@
5658
57from charmhelpers.contrib.openstack.utils import get_host_ip59from charmhelpers.contrib.openstack.utils import get_host_ip
5860
61from charmhelpers.contrib.charmsupport import nrpe
62
59hooks = Hooks()63hooks = Hooks()
6064
61COROSYNC_CONF = '/etc/corosync/corosync.conf'65COROSYNC_CONF = '/etc/corosync/corosync.conf'
@@ -68,7 +72,8 @@
68 COROSYNC_CONF72 COROSYNC_CONF
69]73]
7074
71PACKAGES = ['corosync', 'pacemaker', 'python-netaddr', 'ipmitool']75PACKAGES = ['corosync', 'pacemaker', 'python-netaddr', 'ipmitool',
76 'libnagios-plugin-perl']
72SUPPORTED_TRANSPORTS = ['udp', 'udpu', 'multicast', 'unicast']77SUPPORTED_TRANSPORTS = ['udp', 'udpu', 'multicast', 'unicast']
7378
7479
@@ -207,11 +212,15 @@
207 configure_monitor_host()212 configure_monitor_host()
208 configure_stonith()213 configure_stonith()
209214
215 update_nrpe_config()
216
210217
211@hooks.hook()218@hooks.hook()
212def upgrade_charm():219def upgrade_charm():
213 install()220 install()
214221
222 update_nrpe_config()
223
215224
216def restart_corosync():225def restart_corosync():
217 if service_running("pacemaker"):226 if service_running("pacemaker"):
@@ -582,6 +591,57 @@
582 "versions less than Trusty 14.04")591 "versions less than Trusty 14.04")
583592
584593
594@hooks.hook('nrpe-external-master-relation-joined',
595 'nrpe-external-master-relation-changed')
596def update_nrpe_config():
597 scripts_src = os.path.join(os.environ["CHARM_DIR"], "files",
598 "nrpe")
599 scripts_dst = "/usr/local/lib/nagios/plugins"
600 if not os.path.exists(scripts_dst):
601 os.makedirs(scripts_dst)
602 for fname in glob.glob(os.path.join(scripts_src, "*")):
603 if os.path.isfile(fname):
604 shutil.copy2(fname,
605 os.path.join(scripts_dst, os.path.basename(fname)))
606
607 sudoers_src = os.path.join(os.environ["CHARM_DIR"], "files",
608 "sudoers")
609 sudoers_dst = "/etc/sudoers.d"
610 for fname in glob.glob(os.path.join(sudoers_src, "*")):
611 if os.path.isfile(fname):
612 shutil.copy2(fname,
613 os.path.join(sudoers_dst, os.path.basename(fname)))
614
615 hostname = nrpe.get_nagios_hostname()
616 current_unit = nrpe.get_nagios_unit_name()
617
618 nrpe_setup = nrpe.NRPE(hostname=hostname)
619
620 apt_install('python-dbus')
621
622 # haproxy checks
623 nrpe_setup.add_check(
624 shortname='haproxy_servers',
625 description='Check HAProxy {%s}' % current_unit,
626 check_cmd='check_haproxy.sh')
627 nrpe_setup.add_check(
628 shortname='haproxy_queue',
629 description='Check HAProxy queue depth {%s}' % current_unit,
630 check_cmd='check_haproxy_queue_depth.sh')
631
632 # corosync/crm checks
633 nrpe_setup.add_check(
634 shortname='corosync_rings',
635 description='Check Corosync rings {%s}' % current_unit,
636 check_cmd='check_corosync_rings')
637 nrpe_setup.add_check(
638 shortname='crm_status',
639 description='Check crm status {%s}' % current_unit,
640 check_cmd='check_crm')
641
642 nrpe_setup.write()
643
644
585if __name__ == '__main__':645if __name__ == '__main__':
586 try:646 try:
587 hooks.execute(sys.argv)647 hooks.execute(sys.argv)
588648
=== added symlink 'hooks/nrpe-external-master-relation-changed'
=== target is u'hooks.py'
=== added symlink 'hooks/nrpe-external-master-relation-joined'
=== target is u'hooks.py'
=== modified file 'metadata.yaml'
--- metadata.yaml 2014-04-11 11:22:46 +0000
+++ metadata.yaml 2015-02-12 04:08:21 +0000
@@ -14,6 +14,9 @@
14 ha:14 ha:
15 interface: hacluster15 interface: hacluster
16 scope: container16 scope: container
17 nrpe-external-master:
18 interface: nrpe-external-master
19 scope: container
17peers:20peers:
18 hanode:21 hanode:
19 interface: hacluster22 interface: hacluster

Subscribers

People subscribed via source and target branches