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