Merge lp:~brad-marshall/charms/trusty/landscape-client/add-nrpe-checks into lp:charms/trusty/landscape-client
- Trusty Tahr (14.04)
- add-nrpe-checks
- Merge into trunk
Proposed by
Brad Marshall
Status: | Needs review |
---|---|
Proposed branch: | lp:~brad-marshall/charms/trusty/landscape-client/add-nrpe-checks |
Merge into: | lp:charms/trusty/landscape-client |
Diff against target: |
2627 lines (+1805/-104) 25 files modified
charm-helpers-sync.yaml (+1/-0) config.yaml (+16/-0) hooks/charmhelpers/__init__.py (+38/-0) hooks/charmhelpers/contrib/__init__.py (+15/-0) hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0) hooks/charmhelpers/contrib/charmsupport/nrpe.py (+358/-0) hooks/charmhelpers/contrib/charmsupport/volumes.py (+175/-0) hooks/charmhelpers/core/__init__.py (+15/-0) hooks/charmhelpers/core/decorators.py (+57/-0) hooks/charmhelpers/core/fstab.py (+30/-12) hooks/charmhelpers/core/hookenv.py (+83/-15) hooks/charmhelpers/core/host.py (+97/-32) hooks/charmhelpers/core/services/__init__.py (+18/-2) hooks/charmhelpers/core/services/base.py (+16/-0) hooks/charmhelpers/core/services/helpers.py (+37/-9) hooks/charmhelpers/core/strutils.py (+42/-0) hooks/charmhelpers/core/sysctl.py (+56/-0) hooks/charmhelpers/core/templating.py (+20/-3) hooks/charmhelpers/core/unitdata.py (+477/-0) hooks/charmhelpers/fetch/__init__.py (+42/-13) hooks/charmhelpers/fetch/archiveurl.py (+69/-16) hooks/charmhelpers/fetch/bzrurl.py (+30/-2) hooks/charmhelpers/fetch/giturl.py (+71/-0) hooks/hooks.py (+23/-0) metadata.yaml (+4/-0) |
To merge this branch: | bzr merge lp:~brad-marshall/charms/trusty/landscape-client/add-nrpe-checks |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Landscape | Pending | ||
Review via email: mp+254693@code.launchpad.net |
Commit message
Description of the change
Add basic NRPE checks to landscape-client charm. Currently just checks the return from the init script, and that at least one landscape-client process is running.
To post a comment you must log in.
Unmerged revisions
- 47. By Brad Marshall
-
[bradm] Make the proc check for at least one, rather than exactly one.
- 46. By Brad Marshall
-
[bradm] Look for landscape-client as an argument, not a full command
- 45. By Brad Marshall
-
[bradm] Moved nrpe config update to before the return in the config-changed hook function
- 44. By Brad Marshall
-
[bradm] Import apt_install function from charmhelpers.fetch
- 43. By Brad Marshall
-
[bradm] Initial nagios checks, sync charmhelpers
- 42. By Alberto Donato
-
Merge from lp:landscape-client-charm.
- 41. By David Britton
-
sync merge history from trunk (no file differences) [trivial]
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charm-helpers-sync.yaml' | |||
2 | --- charm-helpers-sync.yaml 2014-05-11 12:16:14 +0000 | |||
3 | +++ charm-helpers-sync.yaml 2015-03-31 04:43:29 +0000 | |||
4 | @@ -3,6 +3,7 @@ | |||
5 | 3 | include: | 3 | include: |
6 | 4 | - core | 4 | - core |
7 | 5 | - fetch | 5 | - fetch |
8 | 6 | - contrib.charmsupport | ||
9 | 6 | # - contrib.storage.linux: | 7 | # - contrib.storage.linux: |
10 | 7 | # - utils | 8 | # - utils |
11 | 8 | # - payload.execd | 9 | # - payload.execd |
12 | 9 | 10 | ||
13 | === modified file 'config.yaml' | |||
14 | --- config.yaml 2015-01-22 15:17:28 +0000 | |||
15 | +++ config.yaml 2015-03-31 04:43:29 +0000 | |||
16 | @@ -104,3 +104,19 @@ | |||
17 | 104 | all plugins. | 104 | all plugins. |
18 | 105 | type: string | 105 | type: string |
19 | 106 | default: ALL | 106 | default: ALL |
20 | 107 | nagios_context: | ||
21 | 108 | default: "juju" | ||
22 | 109 | type: string | ||
23 | 110 | description: | | ||
24 | 111 | Used by the nrpe-external-master subordinate charm. | ||
25 | 112 | A string that will be prepended to instance name to set the host name | ||
26 | 113 | in nagios. So for instance the hostname would be something like: | ||
27 | 114 | juju-myservice-0 | ||
28 | 115 | If you're running multiple environments with the same services in them | ||
29 | 116 | this allows you to differentiate between them. | ||
30 | 117 | nagios_servicegroups: | ||
31 | 118 | default: "" | ||
32 | 119 | type: string | ||
33 | 120 | description: | | ||
34 | 121 | A comma-separated list of nagios servicegroups. | ||
35 | 122 | If left empty, the nagios_context will be used as the servicegroup | ||
36 | 107 | 123 | ||
37 | === modified file 'hooks/charmhelpers/__init__.py' | |||
38 | --- hooks/charmhelpers/__init__.py 2014-05-12 10:36:48 +0000 | |||
39 | +++ hooks/charmhelpers/__init__.py 2015-03-31 04:43:29 +0000 | |||
40 | @@ -0,0 +1,38 @@ | |||
41 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
42 | 2 | # | ||
43 | 3 | # This file is part of charm-helpers. | ||
44 | 4 | # | ||
45 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
46 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
47 | 7 | # published by the Free Software Foundation. | ||
48 | 8 | # | ||
49 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
50 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
51 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
52 | 12 | # GNU Lesser General Public License for more details. | ||
53 | 13 | # | ||
54 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
55 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
56 | 16 | |||
57 | 17 | # Bootstrap charm-helpers, installing its dependencies if necessary using | ||
58 | 18 | # only standard libraries. | ||
59 | 19 | import subprocess | ||
60 | 20 | import sys | ||
61 | 21 | |||
62 | 22 | try: | ||
63 | 23 | import six # flake8: noqa | ||
64 | 24 | except ImportError: | ||
65 | 25 | if sys.version_info.major == 2: | ||
66 | 26 | subprocess.check_call(['apt-get', 'install', '-y', 'python-six']) | ||
67 | 27 | else: | ||
68 | 28 | subprocess.check_call(['apt-get', 'install', '-y', 'python3-six']) | ||
69 | 29 | import six # flake8: noqa | ||
70 | 30 | |||
71 | 31 | try: | ||
72 | 32 | import yaml # flake8: noqa | ||
73 | 33 | except ImportError: | ||
74 | 34 | if sys.version_info.major == 2: | ||
75 | 35 | subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml']) | ||
76 | 36 | else: | ||
77 | 37 | subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml']) | ||
78 | 38 | import yaml # flake8: noqa | ||
79 | 0 | 39 | ||
80 | === added directory 'hooks/charmhelpers/contrib' | |||
81 | === added file 'hooks/charmhelpers/contrib/__init__.py' | |||
82 | --- hooks/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000 | |||
83 | +++ hooks/charmhelpers/contrib/__init__.py 2015-03-31 04:43:29 +0000 | |||
84 | @@ -0,0 +1,15 @@ | |||
85 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
86 | 2 | # | ||
87 | 3 | # This file is part of charm-helpers. | ||
88 | 4 | # | ||
89 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
90 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
91 | 7 | # published by the Free Software Foundation. | ||
92 | 8 | # | ||
93 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
94 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
95 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
96 | 12 | # GNU Lesser General Public License for more details. | ||
97 | 13 | # | ||
98 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
99 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
100 | 0 | 16 | ||
101 | === added directory 'hooks/charmhelpers/contrib/charmsupport' | |||
102 | === added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py' | |||
103 | --- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000 | |||
104 | +++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-03-31 04:43:29 +0000 | |||
105 | @@ -0,0 +1,15 @@ | |||
106 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
107 | 2 | # | ||
108 | 3 | # This file is part of charm-helpers. | ||
109 | 4 | # | ||
110 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
111 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
112 | 7 | # published by the Free Software Foundation. | ||
113 | 8 | # | ||
114 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
115 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
116 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
117 | 12 | # GNU Lesser General Public License for more details. | ||
118 | 13 | # | ||
119 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
120 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
121 | 0 | 16 | ||
122 | === added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py' | |||
123 | --- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 | |||
124 | +++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-31 04:43:29 +0000 | |||
125 | @@ -0,0 +1,358 @@ | |||
126 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
127 | 2 | # | ||
128 | 3 | # This file is part of charm-helpers. | ||
129 | 4 | # | ||
130 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
131 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
132 | 7 | # published by the Free Software Foundation. | ||
133 | 8 | # | ||
134 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
135 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
136 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
137 | 12 | # GNU Lesser General Public License for more details. | ||
138 | 13 | # | ||
139 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
140 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
141 | 16 | |||
142 | 17 | """Compatibility with the nrpe-external-master charm""" | ||
143 | 18 | # Copyright 2012 Canonical Ltd. | ||
144 | 19 | # | ||
145 | 20 | # Authors: | ||
146 | 21 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
147 | 22 | |||
148 | 23 | import subprocess | ||
149 | 24 | import pwd | ||
150 | 25 | import grp | ||
151 | 26 | import os | ||
152 | 27 | import glob | ||
153 | 28 | import shutil | ||
154 | 29 | import re | ||
155 | 30 | import shlex | ||
156 | 31 | import yaml | ||
157 | 32 | |||
158 | 33 | from charmhelpers.core.hookenv import ( | ||
159 | 34 | config, | ||
160 | 35 | local_unit, | ||
161 | 36 | log, | ||
162 | 37 | relation_ids, | ||
163 | 38 | relation_set, | ||
164 | 39 | relations_of_type, | ||
165 | 40 | ) | ||
166 | 41 | |||
167 | 42 | from charmhelpers.core.host import service | ||
168 | 43 | |||
169 | 44 | # This module adds compatibility with the nrpe-external-master and plain nrpe | ||
170 | 45 | # subordinate charms. To use it in your charm: | ||
171 | 46 | # | ||
172 | 47 | # 1. Update metadata.yaml | ||
173 | 48 | # | ||
174 | 49 | # provides: | ||
175 | 50 | # (...) | ||
176 | 51 | # nrpe-external-master: | ||
177 | 52 | # interface: nrpe-external-master | ||
178 | 53 | # scope: container | ||
179 | 54 | # | ||
180 | 55 | # and/or | ||
181 | 56 | # | ||
182 | 57 | # provides: | ||
183 | 58 | # (...) | ||
184 | 59 | # local-monitors: | ||
185 | 60 | # interface: local-monitors | ||
186 | 61 | # scope: container | ||
187 | 62 | |||
188 | 63 | # | ||
189 | 64 | # 2. Add the following to config.yaml | ||
190 | 65 | # | ||
191 | 66 | # nagios_context: | ||
192 | 67 | # default: "juju" | ||
193 | 68 | # type: string | ||
194 | 69 | # description: | | ||
195 | 70 | # Used by the nrpe subordinate charms. | ||
196 | 71 | # A string that will be prepended to instance name to set the host name | ||
197 | 72 | # in nagios. So for instance the hostname would be something like: | ||
198 | 73 | # juju-myservice-0 | ||
199 | 74 | # If you're running multiple environments with the same services in them | ||
200 | 75 | # this allows you to differentiate between them. | ||
201 | 76 | # nagios_servicegroups: | ||
202 | 77 | # default: "" | ||
203 | 78 | # type: string | ||
204 | 79 | # description: | | ||
205 | 80 | # A comma-separated list of nagios servicegroups. | ||
206 | 81 | # If left empty, the nagios_context will be used as the servicegroup | ||
207 | 82 | # | ||
208 | 83 | # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master | ||
209 | 84 | # | ||
210 | 85 | # 4. Update your hooks.py with something like this: | ||
211 | 86 | # | ||
212 | 87 | # from charmsupport.nrpe import NRPE | ||
213 | 88 | # (...) | ||
214 | 89 | # def update_nrpe_config(): | ||
215 | 90 | # nrpe_compat = NRPE() | ||
216 | 91 | # nrpe_compat.add_check( | ||
217 | 92 | # shortname = "myservice", | ||
218 | 93 | # description = "Check MyService", | ||
219 | 94 | # check_cmd = "check_http -w 2 -c 10 http://localhost" | ||
220 | 95 | # ) | ||
221 | 96 | # nrpe_compat.add_check( | ||
222 | 97 | # "myservice_other", | ||
223 | 98 | # "Check for widget failures", | ||
224 | 99 | # check_cmd = "/srv/myapp/scripts/widget_check" | ||
225 | 100 | # ) | ||
226 | 101 | # nrpe_compat.write() | ||
227 | 102 | # | ||
228 | 103 | # def config_changed(): | ||
229 | 104 | # (...) | ||
230 | 105 | # update_nrpe_config() | ||
231 | 106 | # | ||
232 | 107 | # def nrpe_external_master_relation_changed(): | ||
233 | 108 | # update_nrpe_config() | ||
234 | 109 | # | ||
235 | 110 | # def local_monitors_relation_changed(): | ||
236 | 111 | # update_nrpe_config() | ||
237 | 112 | # | ||
238 | 113 | # 5. ln -s hooks.py nrpe-external-master-relation-changed | ||
239 | 114 | # ln -s hooks.py local-monitors-relation-changed | ||
240 | 115 | |||
241 | 116 | |||
242 | 117 | class CheckException(Exception): | ||
243 | 118 | pass | ||
244 | 119 | |||
245 | 120 | |||
246 | 121 | class Check(object): | ||
247 | 122 | shortname_re = '[A-Za-z0-9-_]+$' | ||
248 | 123 | service_template = (""" | ||
249 | 124 | #--------------------------------------------------- | ||
250 | 125 | # This file is Juju managed | ||
251 | 126 | #--------------------------------------------------- | ||
252 | 127 | define service {{ | ||
253 | 128 | use active-service | ||
254 | 129 | host_name {nagios_hostname} | ||
255 | 130 | service_description {nagios_hostname}[{shortname}] """ | ||
256 | 131 | """{description} | ||
257 | 132 | check_command check_nrpe!{command} | ||
258 | 133 | servicegroups {nagios_servicegroup} | ||
259 | 134 | }} | ||
260 | 135 | """) | ||
261 | 136 | |||
262 | 137 | def __init__(self, shortname, description, check_cmd): | ||
263 | 138 | super(Check, self).__init__() | ||
264 | 139 | # XXX: could be better to calculate this from the service name | ||
265 | 140 | if not re.match(self.shortname_re, shortname): | ||
266 | 141 | raise CheckException("shortname must match {}".format( | ||
267 | 142 | Check.shortname_re)) | ||
268 | 143 | self.shortname = shortname | ||
269 | 144 | self.command = "check_{}".format(shortname) | ||
270 | 145 | # Note: a set of invalid characters is defined by the | ||
271 | 146 | # Nagios server config | ||
272 | 147 | # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= | ||
273 | 148 | self.description = description | ||
274 | 149 | self.check_cmd = self._locate_cmd(check_cmd) | ||
275 | 150 | |||
276 | 151 | def _locate_cmd(self, check_cmd): | ||
277 | 152 | search_path = ( | ||
278 | 153 | '/usr/lib/nagios/plugins', | ||
279 | 154 | '/usr/local/lib/nagios/plugins', | ||
280 | 155 | ) | ||
281 | 156 | parts = shlex.split(check_cmd) | ||
282 | 157 | for path in search_path: | ||
283 | 158 | if os.path.exists(os.path.join(path, parts[0])): | ||
284 | 159 | command = os.path.join(path, parts[0]) | ||
285 | 160 | if len(parts) > 1: | ||
286 | 161 | command += " " + " ".join(parts[1:]) | ||
287 | 162 | return command | ||
288 | 163 | log('Check command not found: {}'.format(parts[0])) | ||
289 | 164 | return '' | ||
290 | 165 | |||
291 | 166 | def write(self, nagios_context, hostname, nagios_servicegroups): | ||
292 | 167 | nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format( | ||
293 | 168 | self.command) | ||
294 | 169 | with open(nrpe_check_file, 'w') as nrpe_check_config: | ||
295 | 170 | nrpe_check_config.write("# check {}\n".format(self.shortname)) | ||
296 | 171 | nrpe_check_config.write("command[{}]={}\n".format( | ||
297 | 172 | self.command, self.check_cmd)) | ||
298 | 173 | |||
299 | 174 | if not os.path.exists(NRPE.nagios_exportdir): | ||
300 | 175 | log('Not writing service config as {} is not accessible'.format( | ||
301 | 176 | NRPE.nagios_exportdir)) | ||
302 | 177 | else: | ||
303 | 178 | self.write_service_config(nagios_context, hostname, | ||
304 | 179 | nagios_servicegroups) | ||
305 | 180 | |||
306 | 181 | def write_service_config(self, nagios_context, hostname, | ||
307 | 182 | nagios_servicegroups): | ||
308 | 183 | for f in os.listdir(NRPE.nagios_exportdir): | ||
309 | 184 | if re.search('.*{}.cfg'.format(self.command), f): | ||
310 | 185 | os.remove(os.path.join(NRPE.nagios_exportdir, f)) | ||
311 | 186 | |||
312 | 187 | templ_vars = { | ||
313 | 188 | 'nagios_hostname': hostname, | ||
314 | 189 | 'nagios_servicegroup': nagios_servicegroups, | ||
315 | 190 | 'description': self.description, | ||
316 | 191 | 'shortname': self.shortname, | ||
317 | 192 | 'command': self.command, | ||
318 | 193 | } | ||
319 | 194 | nrpe_service_text = Check.service_template.format(**templ_vars) | ||
320 | 195 | nrpe_service_file = '{}/service__{}_{}.cfg'.format( | ||
321 | 196 | NRPE.nagios_exportdir, hostname, self.command) | ||
322 | 197 | with open(nrpe_service_file, 'w') as nrpe_service_config: | ||
323 | 198 | nrpe_service_config.write(str(nrpe_service_text)) | ||
324 | 199 | |||
325 | 200 | def run(self): | ||
326 | 201 | subprocess.call(self.check_cmd) | ||
327 | 202 | |||
328 | 203 | |||
329 | 204 | class NRPE(object): | ||
330 | 205 | nagios_logdir = '/var/log/nagios' | ||
331 | 206 | nagios_exportdir = '/var/lib/nagios/export' | ||
332 | 207 | nrpe_confdir = '/etc/nagios/nrpe.d' | ||
333 | 208 | |||
334 | 209 | def __init__(self, hostname=None): | ||
335 | 210 | super(NRPE, self).__init__() | ||
336 | 211 | self.config = config() | ||
337 | 212 | self.nagios_context = self.config['nagios_context'] | ||
338 | 213 | if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: | ||
339 | 214 | self.nagios_servicegroups = self.config['nagios_servicegroups'] | ||
340 | 215 | else: | ||
341 | 216 | self.nagios_servicegroups = self.nagios_context | ||
342 | 217 | self.unit_name = local_unit().replace('/', '-') | ||
343 | 218 | if hostname: | ||
344 | 219 | self.hostname = hostname | ||
345 | 220 | else: | ||
346 | 221 | self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) | ||
347 | 222 | self.checks = [] | ||
348 | 223 | |||
349 | 224 | def add_check(self, *args, **kwargs): | ||
350 | 225 | self.checks.append(Check(*args, **kwargs)) | ||
351 | 226 | |||
352 | 227 | def write(self): | ||
353 | 228 | try: | ||
354 | 229 | nagios_uid = pwd.getpwnam('nagios').pw_uid | ||
355 | 230 | nagios_gid = grp.getgrnam('nagios').gr_gid | ||
356 | 231 | except: | ||
357 | 232 | log("Nagios user not set up, nrpe checks not updated") | ||
358 | 233 | return | ||
359 | 234 | |||
360 | 235 | if not os.path.exists(NRPE.nagios_logdir): | ||
361 | 236 | os.mkdir(NRPE.nagios_logdir) | ||
362 | 237 | os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) | ||
363 | 238 | |||
364 | 239 | nrpe_monitors = {} | ||
365 | 240 | monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} | ||
366 | 241 | for nrpecheck in self.checks: | ||
367 | 242 | nrpecheck.write(self.nagios_context, self.hostname, | ||
368 | 243 | self.nagios_servicegroups) | ||
369 | 244 | nrpe_monitors[nrpecheck.shortname] = { | ||
370 | 245 | "command": nrpecheck.command, | ||
371 | 246 | } | ||
372 | 247 | |||
373 | 248 | service('restart', 'nagios-nrpe-server') | ||
374 | 249 | |||
375 | 250 | for rid in relation_ids("local-monitors"): | ||
376 | 251 | relation_set(relation_id=rid, monitors=yaml.dump(monitors)) | ||
377 | 252 | |||
378 | 253 | |||
379 | 254 | def get_nagios_hostcontext(relation_name='nrpe-external-master'): | ||
380 | 255 | """ | ||
381 | 256 | Query relation with nrpe subordinate, return the nagios_host_context | ||
382 | 257 | |||
383 | 258 | :param str relation_name: Name of relation nrpe sub joined to | ||
384 | 259 | """ | ||
385 | 260 | for rel in relations_of_type(relation_name): | ||
386 | 261 | if 'nagios_hostname' in rel: | ||
387 | 262 | return rel['nagios_host_context'] | ||
388 | 263 | |||
389 | 264 | |||
390 | 265 | def get_nagios_hostname(relation_name='nrpe-external-master'): | ||
391 | 266 | """ | ||
392 | 267 | Query relation with nrpe subordinate, return the nagios_hostname | ||
393 | 268 | |||
394 | 269 | :param str relation_name: Name of relation nrpe sub joined to | ||
395 | 270 | """ | ||
396 | 271 | for rel in relations_of_type(relation_name): | ||
397 | 272 | if 'nagios_hostname' in rel: | ||
398 | 273 | return rel['nagios_hostname'] | ||
399 | 274 | |||
400 | 275 | |||
401 | 276 | def get_nagios_unit_name(relation_name='nrpe-external-master'): | ||
402 | 277 | """ | ||
403 | 278 | Return the nagios unit name prepended with host_context if needed | ||
404 | 279 | |||
405 | 280 | :param str relation_name: Name of relation nrpe sub joined to | ||
406 | 281 | """ | ||
407 | 282 | host_context = get_nagios_hostcontext(relation_name) | ||
408 | 283 | if host_context: | ||
409 | 284 | unit = "%s:%s" % (host_context, local_unit()) | ||
410 | 285 | else: | ||
411 | 286 | unit = local_unit() | ||
412 | 287 | return unit | ||
413 | 288 | |||
414 | 289 | |||
415 | 290 | def add_init_service_checks(nrpe, services, unit_name): | ||
416 | 291 | """ | ||
417 | 292 | Add checks for each service in list | ||
418 | 293 | |||
419 | 294 | :param NRPE nrpe: NRPE object to add check to | ||
420 | 295 | :param list services: List of services to check | ||
421 | 296 | :param str unit_name: Unit name to use in check description | ||
422 | 297 | """ | ||
423 | 298 | for svc in services: | ||
424 | 299 | upstart_init = '/etc/init/%s.conf' % svc | ||
425 | 300 | sysv_init = '/etc/init.d/%s' % svc | ||
426 | 301 | if os.path.exists(upstart_init): | ||
427 | 302 | nrpe.add_check( | ||
428 | 303 | shortname=svc, | ||
429 | 304 | description='process check {%s}' % unit_name, | ||
430 | 305 | check_cmd='check_upstart_job %s' % svc | ||
431 | 306 | ) | ||
432 | 307 | elif os.path.exists(sysv_init): | ||
433 | 308 | cronpath = '/etc/cron.d/nagios-service-check-%s' % svc | ||
434 | 309 | cron_file = ('*/5 * * * * root ' | ||
435 | 310 | '/usr/local/lib/nagios/plugins/check_exit_status.pl ' | ||
436 | 311 | '-s /etc/init.d/%s status > ' | ||
437 | 312 | '/var/lib/nagios/service-check-%s.txt\n' % (svc, | ||
438 | 313 | svc) | ||
439 | 314 | ) | ||
440 | 315 | f = open(cronpath, 'w') | ||
441 | 316 | f.write(cron_file) | ||
442 | 317 | f.close() | ||
443 | 318 | nrpe.add_check( | ||
444 | 319 | shortname=svc, | ||
445 | 320 | description='process check {%s}' % unit_name, | ||
446 | 321 | check_cmd='check_status_file.py -f ' | ||
447 | 322 | '/var/lib/nagios/service-check-%s.txt' % svc, | ||
448 | 323 | ) | ||
449 | 324 | |||
450 | 325 | |||
451 | 326 | def copy_nrpe_checks(): | ||
452 | 327 | """ | ||
453 | 328 | Copy the nrpe checks into place | ||
454 | 329 | |||
455 | 330 | """ | ||
456 | 331 | NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' | ||
457 | 332 | nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks', | ||
458 | 333 | 'charmhelpers', 'contrib', 'openstack', | ||
459 | 334 | 'files') | ||
460 | 335 | |||
461 | 336 | if not os.path.exists(NAGIOS_PLUGINS): | ||
462 | 337 | os.makedirs(NAGIOS_PLUGINS) | ||
463 | 338 | for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")): | ||
464 | 339 | if os.path.isfile(fname): | ||
465 | 340 | shutil.copy2(fname, | ||
466 | 341 | os.path.join(NAGIOS_PLUGINS, os.path.basename(fname))) | ||
467 | 342 | |||
468 | 343 | |||
469 | 344 | def add_haproxy_checks(nrpe, unit_name): | ||
470 | 345 | """ | ||
471 | 346 | Add checks for each service in list | ||
472 | 347 | |||
473 | 348 | :param NRPE nrpe: NRPE object to add check to | ||
474 | 349 | :param str unit_name: Unit name to use in check description | ||
475 | 350 | """ | ||
476 | 351 | nrpe.add_check( | ||
477 | 352 | shortname='haproxy_servers', | ||
478 | 353 | description='Check HAProxy {%s}' % unit_name, | ||
479 | 354 | check_cmd='check_haproxy.sh') | ||
480 | 355 | nrpe.add_check( | ||
481 | 356 | shortname='haproxy_queue', | ||
482 | 357 | description='Check HAProxy queue depth {%s}' % unit_name, | ||
483 | 358 | check_cmd='check_haproxy_queue_depth.sh') | ||
484 | 0 | 359 | ||
485 | === added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py' | |||
486 | --- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 | |||
487 | +++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-03-31 04:43:29 +0000 | |||
488 | @@ -0,0 +1,175 @@ | |||
489 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
490 | 2 | # | ||
491 | 3 | # This file is part of charm-helpers. | ||
492 | 4 | # | ||
493 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
494 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
495 | 7 | # published by the Free Software Foundation. | ||
496 | 8 | # | ||
497 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
498 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
499 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
500 | 12 | # GNU Lesser General Public License for more details. | ||
501 | 13 | # | ||
502 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
503 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
504 | 16 | |||
505 | 17 | ''' | ||
506 | 18 | Functions for managing volumes in juju units. One volume is supported per unit. | ||
507 | 19 | Subordinates may have their own storage, provided it is on its own partition. | ||
508 | 20 | |||
509 | 21 | Configuration stanzas:: | ||
510 | 22 | |||
511 | 23 | volume-ephemeral: | ||
512 | 24 | type: boolean | ||
513 | 25 | default: true | ||
514 | 26 | description: > | ||
515 | 27 | If false, a volume is mounted as sepecified in "volume-map" | ||
516 | 28 | If true, ephemeral storage will be used, meaning that log data | ||
517 | 29 | will only exist as long as the machine. YOU HAVE BEEN WARNED. | ||
518 | 30 | volume-map: | ||
519 | 31 | type: string | ||
520 | 32 | default: {} | ||
521 | 33 | description: > | ||
522 | 34 | YAML map of units to device names, e.g: | ||
523 | 35 | "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" | ||
524 | 36 | Service units will raise a configure-error if volume-ephemeral | ||
525 | 37 | is 'true' and no volume-map value is set. Use 'juju set' to set a | ||
526 | 38 | value and 'juju resolved' to complete configuration. | ||
527 | 39 | |||
528 | 40 | Usage:: | ||
529 | 41 | |||
530 | 42 | from charmsupport.volumes import configure_volume, VolumeConfigurationError | ||
531 | 43 | from charmsupport.hookenv import log, ERROR | ||
532 | 44 | def post_mount_hook(): | ||
533 | 45 | stop_service('myservice') | ||
534 | 46 | def post_mount_hook(): | ||
535 | 47 | start_service('myservice') | ||
536 | 48 | |||
537 | 49 | if __name__ == '__main__': | ||
538 | 50 | try: | ||
539 | 51 | configure_volume(before_change=pre_mount_hook, | ||
540 | 52 | after_change=post_mount_hook) | ||
541 | 53 | except VolumeConfigurationError: | ||
542 | 54 | log('Storage could not be configured', ERROR) | ||
543 | 55 | |||
544 | 56 | ''' | ||
545 | 57 | |||
546 | 58 | # XXX: Known limitations | ||
547 | 59 | # - fstab is neither consulted nor updated | ||
548 | 60 | |||
549 | 61 | import os | ||
550 | 62 | from charmhelpers.core import hookenv | ||
551 | 63 | from charmhelpers.core import host | ||
552 | 64 | import yaml | ||
553 | 65 | |||
554 | 66 | |||
555 | 67 | MOUNT_BASE = '/srv/juju/volumes' | ||
556 | 68 | |||
557 | 69 | |||
558 | 70 | class VolumeConfigurationError(Exception): | ||
559 | 71 | '''Volume configuration data is missing or invalid''' | ||
560 | 72 | pass | ||
561 | 73 | |||
562 | 74 | |||
563 | 75 | def get_config(): | ||
564 | 76 | '''Gather and sanity-check volume configuration data''' | ||
565 | 77 | volume_config = {} | ||
566 | 78 | config = hookenv.config() | ||
567 | 79 | |||
568 | 80 | errors = False | ||
569 | 81 | |||
570 | 82 | if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): | ||
571 | 83 | volume_config['ephemeral'] = True | ||
572 | 84 | else: | ||
573 | 85 | volume_config['ephemeral'] = False | ||
574 | 86 | |||
575 | 87 | try: | ||
576 | 88 | volume_map = yaml.safe_load(config.get('volume-map', '{}')) | ||
577 | 89 | except yaml.YAMLError as e: | ||
578 | 90 | hookenv.log("Error parsing YAML volume-map: {}".format(e), | ||
579 | 91 | hookenv.ERROR) | ||
580 | 92 | errors = True | ||
581 | 93 | if volume_map is None: | ||
582 | 94 | # probably an empty string | ||
583 | 95 | volume_map = {} | ||
584 | 96 | elif not isinstance(volume_map, dict): | ||
585 | 97 | hookenv.log("Volume-map should be a dictionary, not {}".format( | ||
586 | 98 | type(volume_map))) | ||
587 | 99 | errors = True | ||
588 | 100 | |||
589 | 101 | volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) | ||
590 | 102 | if volume_config['device'] and volume_config['ephemeral']: | ||
591 | 103 | # asked for ephemeral storage but also defined a volume ID | ||
592 | 104 | hookenv.log('A volume is defined for this unit, but ephemeral ' | ||
593 | 105 | 'storage was requested', hookenv.ERROR) | ||
594 | 106 | errors = True | ||
595 | 107 | elif not volume_config['device'] and not volume_config['ephemeral']: | ||
596 | 108 | # asked for permanent storage but did not define volume ID | ||
597 | 109 | hookenv.log('Ephemeral storage was requested, but there is no volume ' | ||
598 | 110 | 'defined for this unit.', hookenv.ERROR) | ||
599 | 111 | errors = True | ||
600 | 112 | |||
601 | 113 | unit_mount_name = hookenv.local_unit().replace('/', '-') | ||
602 | 114 | volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) | ||
603 | 115 | |||
604 | 116 | if errors: | ||
605 | 117 | return None | ||
606 | 118 | return volume_config | ||
607 | 119 | |||
608 | 120 | |||
609 | 121 | def mount_volume(config): | ||
610 | 122 | if os.path.exists(config['mountpoint']): | ||
611 | 123 | if not os.path.isdir(config['mountpoint']): | ||
612 | 124 | hookenv.log('Not a directory: {}'.format(config['mountpoint'])) | ||
613 | 125 | raise VolumeConfigurationError() | ||
614 | 126 | else: | ||
615 | 127 | host.mkdir(config['mountpoint']) | ||
616 | 128 | if os.path.ismount(config['mountpoint']): | ||
617 | 129 | unmount_volume(config) | ||
618 | 130 | if not host.mount(config['device'], config['mountpoint'], persist=True): | ||
619 | 131 | raise VolumeConfigurationError() | ||
620 | 132 | |||
621 | 133 | |||
622 | 134 | def unmount_volume(config): | ||
623 | 135 | if os.path.ismount(config['mountpoint']): | ||
624 | 136 | if not host.umount(config['mountpoint'], persist=True): | ||
625 | 137 | raise VolumeConfigurationError() | ||
626 | 138 | |||
627 | 139 | |||
628 | 140 | def managed_mounts(): | ||
629 | 141 | '''List of all mounted managed volumes''' | ||
630 | 142 | return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) | ||
631 | 143 | |||
632 | 144 | |||
633 | 145 | def configure_volume(before_change=lambda: None, after_change=lambda: None): | ||
634 | 146 | '''Set up storage (or don't) according to the charm's volume configuration. | ||
635 | 147 | Returns the mount point or "ephemeral". before_change and after_change | ||
636 | 148 | are optional functions to be called if the volume configuration changes. | ||
637 | 149 | ''' | ||
638 | 150 | |||
639 | 151 | config = get_config() | ||
640 | 152 | if not config: | ||
641 | 153 | hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) | ||
642 | 154 | raise VolumeConfigurationError() | ||
643 | 155 | |||
644 | 156 | if config['ephemeral']: | ||
645 | 157 | if os.path.ismount(config['mountpoint']): | ||
646 | 158 | before_change() | ||
647 | 159 | unmount_volume(config) | ||
648 | 160 | after_change() | ||
649 | 161 | return 'ephemeral' | ||
650 | 162 | else: | ||
651 | 163 | # persistent storage | ||
652 | 164 | if os.path.ismount(config['mountpoint']): | ||
653 | 165 | mounts = dict(managed_mounts()) | ||
654 | 166 | if mounts.get(config['mountpoint']) != config['device']: | ||
655 | 167 | before_change() | ||
656 | 168 | unmount_volume(config) | ||
657 | 169 | mount_volume(config) | ||
658 | 170 | after_change() | ||
659 | 171 | else: | ||
660 | 172 | before_change() | ||
661 | 173 | mount_volume(config) | ||
662 | 174 | after_change() | ||
663 | 175 | return config['mountpoint'] | ||
664 | 0 | 176 | ||
665 | === modified file 'hooks/charmhelpers/core/__init__.py' | |||
666 | --- hooks/charmhelpers/core/__init__.py 2014-05-12 10:36:48 +0000 | |||
667 | +++ hooks/charmhelpers/core/__init__.py 2015-03-31 04:43:29 +0000 | |||
668 | @@ -0,0 +1,15 @@ | |||
669 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
670 | 2 | # | ||
671 | 3 | # This file is part of charm-helpers. | ||
672 | 4 | # | ||
673 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
674 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
675 | 7 | # published by the Free Software Foundation. | ||
676 | 8 | # | ||
677 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
678 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
679 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
680 | 12 | # GNU Lesser General Public License for more details. | ||
681 | 13 | # | ||
682 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
683 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
684 | 0 | 16 | ||
685 | === added file 'hooks/charmhelpers/core/decorators.py' | |||
686 | --- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000 | |||
687 | +++ hooks/charmhelpers/core/decorators.py 2015-03-31 04:43:29 +0000 | |||
688 | @@ -0,0 +1,57 @@ | |||
689 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
690 | 2 | # | ||
691 | 3 | # This file is part of charm-helpers. | ||
692 | 4 | # | ||
693 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
694 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
695 | 7 | # published by the Free Software Foundation. | ||
696 | 8 | # | ||
697 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
698 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
699 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
700 | 12 | # GNU Lesser General Public License for more details. | ||
701 | 13 | # | ||
702 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
703 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
704 | 16 | |||
705 | 17 | # | ||
706 | 18 | # Copyright 2014 Canonical Ltd. | ||
707 | 19 | # | ||
708 | 20 | # Authors: | ||
709 | 21 | # Edward Hope-Morley <opentastic@gmail.com> | ||
710 | 22 | # | ||
711 | 23 | |||
712 | 24 | import time | ||
713 | 25 | |||
714 | 26 | from charmhelpers.core.hookenv import ( | ||
715 | 27 | log, | ||
716 | 28 | INFO, | ||
717 | 29 | ) | ||
718 | 30 | |||
719 | 31 | |||
720 | 32 | def retry_on_exception(num_retries, base_delay=0, exc_type=Exception): | ||
721 | 33 | """If the decorated function raises exception exc_type, allow num_retries | ||
722 | 34 | retry attempts before raise the exception. | ||
723 | 35 | """ | ||
724 | 36 | def _retry_on_exception_inner_1(f): | ||
725 | 37 | def _retry_on_exception_inner_2(*args, **kwargs): | ||
726 | 38 | retries = num_retries | ||
727 | 39 | multiplier = 1 | ||
728 | 40 | while True: | ||
729 | 41 | try: | ||
730 | 42 | return f(*args, **kwargs) | ||
731 | 43 | except exc_type: | ||
732 | 44 | if not retries: | ||
733 | 45 | raise | ||
734 | 46 | |||
735 | 47 | delay = base_delay * multiplier | ||
736 | 48 | multiplier += 1 | ||
737 | 49 | log("Retrying '%s' %d more times (delay=%s)" % | ||
738 | 50 | (f.__name__, retries, delay), level=INFO) | ||
739 | 51 | retries -= 1 | ||
740 | 52 | if delay: | ||
741 | 53 | time.sleep(delay) | ||
742 | 54 | |||
743 | 55 | return _retry_on_exception_inner_2 | ||
744 | 56 | |||
745 | 57 | return _retry_on_exception_inner_1 | ||
746 | 0 | 58 | ||
747 | === modified file 'hooks/charmhelpers/core/fstab.py' | |||
748 | --- hooks/charmhelpers/core/fstab.py 2014-09-26 08:54:54 +0000 | |||
749 | +++ hooks/charmhelpers/core/fstab.py 2015-03-31 04:43:29 +0000 | |||
750 | @@ -1,12 +1,29 @@ | |||
751 | 1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
752 | 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
753 | 3 | 3 | ||
754 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
755 | 5 | # | ||
756 | 6 | # This file is part of charm-helpers. | ||
757 | 7 | # | ||
758 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
759 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
760 | 10 | # published by the Free Software Foundation. | ||
761 | 11 | # | ||
762 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
763 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
764 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
765 | 15 | # GNU Lesser General Public License for more details. | ||
766 | 16 | # | ||
767 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
768 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
769 | 19 | |||
770 | 20 | import io | ||
771 | 21 | import os | ||
772 | 22 | |||
773 | 4 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' | 23 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
774 | 5 | 24 | ||
779 | 6 | import os | 25 | |
780 | 7 | 26 | class Fstab(io.FileIO): | |
777 | 8 | |||
778 | 9 | class Fstab(file): | ||
781 | 10 | """This class extends file in order to implement a file reader/writer | 27 | """This class extends file in order to implement a file reader/writer |
782 | 11 | for file `/etc/fstab` | 28 | for file `/etc/fstab` |
783 | 12 | """ | 29 | """ |
784 | @@ -24,8 +41,8 @@ | |||
785 | 24 | options = "defaults" | 41 | options = "defaults" |
786 | 25 | 42 | ||
787 | 26 | self.options = options | 43 | self.options = options |
790 | 27 | self.d = d | 44 | self.d = int(d) |
791 | 28 | self.p = p | 45 | self.p = int(p) |
792 | 29 | 46 | ||
793 | 30 | def __eq__(self, o): | 47 | def __eq__(self, o): |
794 | 31 | return str(self) == str(o) | 48 | return str(self) == str(o) |
795 | @@ -45,7 +62,7 @@ | |||
796 | 45 | self._path = path | 62 | self._path = path |
797 | 46 | else: | 63 | else: |
798 | 47 | self._path = self.DEFAULT_PATH | 64 | self._path = self.DEFAULT_PATH |
800 | 48 | file.__init__(self, self._path, 'r+') | 65 | super(Fstab, self).__init__(self._path, 'rb+') |
801 | 49 | 66 | ||
802 | 50 | def _hydrate_entry(self, line): | 67 | def _hydrate_entry(self, line): |
803 | 51 | # NOTE: use split with no arguments to split on any | 68 | # NOTE: use split with no arguments to split on any |
804 | @@ -58,8 +75,9 @@ | |||
805 | 58 | def entries(self): | 75 | def entries(self): |
806 | 59 | self.seek(0) | 76 | self.seek(0) |
807 | 60 | for line in self.readlines(): | 77 | for line in self.readlines(): |
808 | 78 | line = line.decode('us-ascii') | ||
809 | 61 | try: | 79 | try: |
811 | 62 | if not line.startswith("#"): | 80 | if line.strip() and not line.strip().startswith("#"): |
812 | 63 | yield self._hydrate_entry(line) | 81 | yield self._hydrate_entry(line) |
813 | 64 | except ValueError: | 82 | except ValueError: |
814 | 65 | pass | 83 | pass |
815 | @@ -75,18 +93,18 @@ | |||
816 | 75 | if self.get_entry_by_attr('device', entry.device): | 93 | if self.get_entry_by_attr('device', entry.device): |
817 | 76 | return False | 94 | return False |
818 | 77 | 95 | ||
820 | 78 | self.write(str(entry) + '\n') | 96 | self.write((str(entry) + '\n').encode('us-ascii')) |
821 | 79 | self.truncate() | 97 | self.truncate() |
822 | 80 | return entry | 98 | return entry |
823 | 81 | 99 | ||
824 | 82 | def remove_entry(self, entry): | 100 | def remove_entry(self, entry): |
825 | 83 | self.seek(0) | 101 | self.seek(0) |
826 | 84 | 102 | ||
828 | 85 | lines = self.readlines() | 103 | lines = [l.decode('us-ascii') for l in self.readlines()] |
829 | 86 | 104 | ||
830 | 87 | found = False | 105 | found = False |
831 | 88 | for index, line in enumerate(lines): | 106 | for index, line in enumerate(lines): |
833 | 89 | if not line.startswith("#"): | 107 | if line.strip() and not line.strip().startswith("#"): |
834 | 90 | if self._hydrate_entry(line) == entry: | 108 | if self._hydrate_entry(line) == entry: |
835 | 91 | found = True | 109 | found = True |
836 | 92 | break | 110 | break |
837 | @@ -97,7 +115,7 @@ | |||
838 | 97 | lines.remove(line) | 115 | lines.remove(line) |
839 | 98 | 116 | ||
840 | 99 | self.seek(0) | 117 | self.seek(0) |
842 | 100 | self.write(''.join(lines)) | 118 | self.write(''.join(lines).encode('us-ascii')) |
843 | 101 | self.truncate() | 119 | self.truncate() |
844 | 102 | return True | 120 | return True |
845 | 103 | 121 | ||
846 | 104 | 122 | ||
847 | === modified file 'hooks/charmhelpers/core/hookenv.py' | |||
848 | --- hooks/charmhelpers/core/hookenv.py 2014-09-26 08:54:54 +0000 | |||
849 | +++ hooks/charmhelpers/core/hookenv.py 2015-03-31 04:43:29 +0000 | |||
850 | @@ -1,3 +1,19 @@ | |||
851 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
852 | 2 | # | ||
853 | 3 | # This file is part of charm-helpers. | ||
854 | 4 | # | ||
855 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
856 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
857 | 7 | # published by the Free Software Foundation. | ||
858 | 8 | # | ||
859 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
860 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
861 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
862 | 12 | # GNU Lesser General Public License for more details. | ||
863 | 13 | # | ||
864 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
865 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
866 | 16 | |||
867 | 1 | "Interactions with the Juju environment" | 17 | "Interactions with the Juju environment" |
868 | 2 | # Copyright 2013 Canonical Ltd. | 18 | # Copyright 2013 Canonical Ltd. |
869 | 3 | # | 19 | # |
870 | @@ -9,9 +25,14 @@ | |||
871 | 9 | import yaml | 25 | import yaml |
872 | 10 | import subprocess | 26 | import subprocess |
873 | 11 | import sys | 27 | import sys |
874 | 12 | import UserDict | ||
875 | 13 | from subprocess import CalledProcessError | 28 | from subprocess import CalledProcessError |
876 | 14 | 29 | ||
877 | 30 | import six | ||
878 | 31 | if not six.PY3: | ||
879 | 32 | from UserDict import UserDict | ||
880 | 33 | else: | ||
881 | 34 | from collections import UserDict | ||
882 | 35 | |||
883 | 15 | CRITICAL = "CRITICAL" | 36 | CRITICAL = "CRITICAL" |
884 | 16 | ERROR = "ERROR" | 37 | ERROR = "ERROR" |
885 | 17 | WARNING = "WARNING" | 38 | WARNING = "WARNING" |
886 | @@ -63,16 +84,18 @@ | |||
887 | 63 | command = ['juju-log'] | 84 | command = ['juju-log'] |
888 | 64 | if level: | 85 | if level: |
889 | 65 | command += ['-l', level] | 86 | command += ['-l', level] |
890 | 87 | if not isinstance(message, six.string_types): | ||
891 | 88 | message = repr(message) | ||
892 | 66 | command += [message] | 89 | command += [message] |
893 | 67 | subprocess.call(command) | 90 | subprocess.call(command) |
894 | 68 | 91 | ||
895 | 69 | 92 | ||
897 | 70 | class Serializable(UserDict.IterableUserDict): | 93 | class Serializable(UserDict): |
898 | 71 | """Wrapper, an object that can be serialized to yaml or json""" | 94 | """Wrapper, an object that can be serialized to yaml or json""" |
899 | 72 | 95 | ||
900 | 73 | def __init__(self, obj): | 96 | def __init__(self, obj): |
901 | 74 | # wrap the object | 97 | # wrap the object |
903 | 75 | UserDict.IterableUserDict.__init__(self) | 98 | UserDict.__init__(self) |
904 | 76 | self.data = obj | 99 | self.data = obj |
905 | 77 | 100 | ||
906 | 78 | def __getattr__(self, attr): | 101 | def __getattr__(self, attr): |
907 | @@ -214,6 +237,12 @@ | |||
908 | 214 | except KeyError: | 237 | except KeyError: |
909 | 215 | return (self._prev_dict or {})[key] | 238 | return (self._prev_dict or {})[key] |
910 | 216 | 239 | ||
911 | 240 | def keys(self): | ||
912 | 241 | prev_keys = [] | ||
913 | 242 | if self._prev_dict is not None: | ||
914 | 243 | prev_keys = self._prev_dict.keys() | ||
915 | 244 | return list(set(prev_keys + list(dict.keys(self)))) | ||
916 | 245 | |||
917 | 217 | def load_previous(self, path=None): | 246 | def load_previous(self, path=None): |
918 | 218 | """Load previous copy of config from disk. | 247 | """Load previous copy of config from disk. |
919 | 219 | 248 | ||
920 | @@ -263,7 +292,7 @@ | |||
921 | 263 | 292 | ||
922 | 264 | """ | 293 | """ |
923 | 265 | if self._prev_dict: | 294 | if self._prev_dict: |
925 | 266 | for k, v in self._prev_dict.iteritems(): | 295 | for k, v in six.iteritems(self._prev_dict): |
926 | 267 | if k not in self: | 296 | if k not in self: |
927 | 268 | self[k] = v | 297 | self[k] = v |
928 | 269 | with open(self.path, 'w') as f: | 298 | with open(self.path, 'w') as f: |
929 | @@ -278,7 +307,8 @@ | |||
930 | 278 | config_cmd_line.append(scope) | 307 | config_cmd_line.append(scope) |
931 | 279 | config_cmd_line.append('--format=json') | 308 | config_cmd_line.append('--format=json') |
932 | 280 | try: | 309 | try: |
934 | 281 | config_data = json.loads(subprocess.check_output(config_cmd_line)) | 310 | config_data = json.loads( |
935 | 311 | subprocess.check_output(config_cmd_line).decode('UTF-8')) | ||
936 | 282 | if scope is not None: | 312 | if scope is not None: |
937 | 283 | return config_data | 313 | return config_data |
938 | 284 | return Config(config_data) | 314 | return Config(config_data) |
939 | @@ -297,10 +327,10 @@ | |||
940 | 297 | if unit: | 327 | if unit: |
941 | 298 | _args.append(unit) | 328 | _args.append(unit) |
942 | 299 | try: | 329 | try: |
944 | 300 | return json.loads(subprocess.check_output(_args)) | 330 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
945 | 301 | except ValueError: | 331 | except ValueError: |
946 | 302 | return None | 332 | return None |
948 | 303 | except CalledProcessError, e: | 333 | except CalledProcessError as e: |
949 | 304 | if e.returncode == 2: | 334 | if e.returncode == 2: |
950 | 305 | return None | 335 | return None |
951 | 306 | raise | 336 | raise |
952 | @@ -312,7 +342,7 @@ | |||
953 | 312 | relation_cmd_line = ['relation-set'] | 342 | relation_cmd_line = ['relation-set'] |
954 | 313 | if relation_id is not None: | 343 | if relation_id is not None: |
955 | 314 | relation_cmd_line.extend(('-r', relation_id)) | 344 | relation_cmd_line.extend(('-r', relation_id)) |
957 | 315 | for k, v in (relation_settings.items() + kwargs.items()): | 345 | for k, v in (list(relation_settings.items()) + list(kwargs.items())): |
958 | 316 | if v is None: | 346 | if v is None: |
959 | 317 | relation_cmd_line.append('{}='.format(k)) | 347 | relation_cmd_line.append('{}='.format(k)) |
960 | 318 | else: | 348 | else: |
961 | @@ -329,7 +359,8 @@ | |||
962 | 329 | relid_cmd_line = ['relation-ids', '--format=json'] | 359 | relid_cmd_line = ['relation-ids', '--format=json'] |
963 | 330 | if reltype is not None: | 360 | if reltype is not None: |
964 | 331 | relid_cmd_line.append(reltype) | 361 | relid_cmd_line.append(reltype) |
966 | 332 | return json.loads(subprocess.check_output(relid_cmd_line)) or [] | 362 | return json.loads( |
967 | 363 | subprocess.check_output(relid_cmd_line).decode('UTF-8')) or [] | ||
968 | 333 | return [] | 364 | return [] |
969 | 334 | 365 | ||
970 | 335 | 366 | ||
971 | @@ -340,7 +371,8 @@ | |||
972 | 340 | units_cmd_line = ['relation-list', '--format=json'] | 371 | units_cmd_line = ['relation-list', '--format=json'] |
973 | 341 | if relid is not None: | 372 | if relid is not None: |
974 | 342 | units_cmd_line.extend(('-r', relid)) | 373 | units_cmd_line.extend(('-r', relid)) |
976 | 343 | return json.loads(subprocess.check_output(units_cmd_line)) or [] | 374 | return json.loads( |
977 | 375 | subprocess.check_output(units_cmd_line).decode('UTF-8')) or [] | ||
978 | 344 | 376 | ||
979 | 345 | 377 | ||
980 | 346 | @cached | 378 | @cached |
981 | @@ -380,21 +412,31 @@ | |||
982 | 380 | 412 | ||
983 | 381 | 413 | ||
984 | 382 | @cached | 414 | @cached |
985 | 415 | def metadata(): | ||
986 | 416 | """Get the current charm metadata.yaml contents as a python object""" | ||
987 | 417 | with open(os.path.join(charm_dir(), 'metadata.yaml')) as md: | ||
988 | 418 | return yaml.safe_load(md) | ||
989 | 419 | |||
990 | 420 | |||
991 | 421 | @cached | ||
992 | 383 | def relation_types(): | 422 | def relation_types(): |
993 | 384 | """Get a list of relation types supported by this charm""" | 423 | """Get a list of relation types supported by this charm""" |
994 | 385 | charmdir = os.environ.get('CHARM_DIR', '') | ||
995 | 386 | mdf = open(os.path.join(charmdir, 'metadata.yaml')) | ||
996 | 387 | md = yaml.safe_load(mdf) | ||
997 | 388 | rel_types = [] | 424 | rel_types = [] |
998 | 425 | md = metadata() | ||
999 | 389 | for key in ('provides', 'requires', 'peers'): | 426 | for key in ('provides', 'requires', 'peers'): |
1000 | 390 | section = md.get(key) | 427 | section = md.get(key) |
1001 | 391 | if section: | 428 | if section: |
1002 | 392 | rel_types.extend(section.keys()) | 429 | rel_types.extend(section.keys()) |
1003 | 393 | mdf.close() | ||
1004 | 394 | return rel_types | 430 | return rel_types |
1005 | 395 | 431 | ||
1006 | 396 | 432 | ||
1007 | 397 | @cached | 433 | @cached |
1008 | 434 | def charm_name(): | ||
1009 | 435 | """Get the name of the current charm as is specified on metadata.yaml""" | ||
1010 | 436 | return metadata().get('name') | ||
1011 | 437 | |||
1012 | 438 | |||
1013 | 439 | @cached | ||
1014 | 398 | def relations(): | 440 | def relations(): |
1015 | 399 | """Get a nested dictionary of relation data for all related units""" | 441 | """Get a nested dictionary of relation data for all related units""" |
1016 | 400 | rels = {} | 442 | rels = {} |
1017 | @@ -449,7 +491,7 @@ | |||
1018 | 449 | """Get the unit ID for the remote unit""" | 491 | """Get the unit ID for the remote unit""" |
1019 | 450 | _args = ['unit-get', '--format=json', attribute] | 492 | _args = ['unit-get', '--format=json', attribute] |
1020 | 451 | try: | 493 | try: |
1022 | 452 | return json.loads(subprocess.check_output(_args)) | 494 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
1023 | 453 | except ValueError: | 495 | except ValueError: |
1024 | 454 | return None | 496 | return None |
1025 | 455 | 497 | ||
1026 | @@ -524,3 +566,29 @@ | |||
1027 | 524 | def charm_dir(): | 566 | def charm_dir(): |
1028 | 525 | """Return the root directory of the current charm""" | 567 | """Return the root directory of the current charm""" |
1029 | 526 | return os.environ.get('CHARM_DIR') | 568 | return os.environ.get('CHARM_DIR') |
1030 | 569 | |||
1031 | 570 | |||
1032 | 571 | @cached | ||
1033 | 572 | def action_get(key=None): | ||
1034 | 573 | """Gets the value of an action parameter, or all key/value param pairs""" | ||
1035 | 574 | cmd = ['action-get'] | ||
1036 | 575 | if key is not None: | ||
1037 | 576 | cmd.append(key) | ||
1038 | 577 | cmd.append('--format=json') | ||
1039 | 578 | action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
1040 | 579 | return action_data | ||
1041 | 580 | |||
1042 | 581 | |||
1043 | 582 | def action_set(values): | ||
1044 | 583 | """Sets the values to be returned after the action finishes""" | ||
1045 | 584 | cmd = ['action-set'] | ||
1046 | 585 | for k, v in list(values.items()): | ||
1047 | 586 | cmd.append('{}={}'.format(k, v)) | ||
1048 | 587 | subprocess.check_call(cmd) | ||
1049 | 588 | |||
1050 | 589 | |||
1051 | 590 | def action_fail(message): | ||
1052 | 591 | """Sets the action status to failed and sets the error message. | ||
1053 | 592 | |||
1054 | 593 | The results set by action_set are preserved.""" | ||
1055 | 594 | subprocess.check_call(['action-fail', message]) | ||
1056 | 527 | 595 | ||
1057 | === modified file 'hooks/charmhelpers/core/host.py' | |||
1058 | --- hooks/charmhelpers/core/host.py 2014-09-26 08:54:54 +0000 | |||
1059 | +++ hooks/charmhelpers/core/host.py 2015-03-31 04:43:29 +0000 | |||
1060 | @@ -1,3 +1,19 @@ | |||
1061 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
1062 | 2 | # | ||
1063 | 3 | # This file is part of charm-helpers. | ||
1064 | 4 | # | ||
1065 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1066 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1067 | 7 | # published by the Free Software Foundation. | ||
1068 | 8 | # | ||
1069 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1070 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1071 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1072 | 12 | # GNU Lesser General Public License for more details. | ||
1073 | 13 | # | ||
1074 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1075 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1076 | 16 | |||
1077 | 1 | """Tools for working with the host system""" | 17 | """Tools for working with the host system""" |
1078 | 2 | # Copyright 2012 Canonical Ltd. | 18 | # Copyright 2012 Canonical Ltd. |
1079 | 3 | # | 19 | # |
1080 | @@ -6,19 +22,20 @@ | |||
1081 | 6 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | 22 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> |
1082 | 7 | 23 | ||
1083 | 8 | import os | 24 | import os |
1084 | 25 | import re | ||
1085 | 9 | import pwd | 26 | import pwd |
1086 | 10 | import grp | 27 | import grp |
1087 | 11 | import random | 28 | import random |
1088 | 12 | import string | 29 | import string |
1089 | 13 | import subprocess | 30 | import subprocess |
1090 | 14 | import hashlib | 31 | import hashlib |
1091 | 15 | import shutil | ||
1092 | 16 | from contextlib import contextmanager | 32 | from contextlib import contextmanager |
1093 | 17 | |||
1094 | 18 | from collections import OrderedDict | 33 | from collections import OrderedDict |
1095 | 19 | 34 | ||
1098 | 20 | from hookenv import log | 35 | import six |
1099 | 21 | from fstab import Fstab | 36 | |
1100 | 37 | from .hookenv import log | ||
1101 | 38 | from .fstab import Fstab | ||
1102 | 22 | 39 | ||
1103 | 23 | 40 | ||
1104 | 24 | def service_start(service_name): | 41 | def service_start(service_name): |
1105 | @@ -54,7 +71,9 @@ | |||
1106 | 54 | def service_running(service): | 71 | def service_running(service): |
1107 | 55 | """Determine whether a system service is running""" | 72 | """Determine whether a system service is running""" |
1108 | 56 | try: | 73 | try: |
1110 | 57 | output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT) | 74 | output = subprocess.check_output( |
1111 | 75 | ['service', service, 'status'], | ||
1112 | 76 | stderr=subprocess.STDOUT).decode('UTF-8') | ||
1113 | 58 | except subprocess.CalledProcessError: | 77 | except subprocess.CalledProcessError: |
1114 | 59 | return False | 78 | return False |
1115 | 60 | else: | 79 | else: |
1116 | @@ -67,7 +86,9 @@ | |||
1117 | 67 | def service_available(service_name): | 86 | def service_available(service_name): |
1118 | 68 | """Determine whether a system service is available""" | 87 | """Determine whether a system service is available""" |
1119 | 69 | try: | 88 | try: |
1121 | 70 | subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT) | 89 | subprocess.check_output( |
1122 | 90 | ['service', service_name, 'status'], | ||
1123 | 91 | stderr=subprocess.STDOUT).decode('UTF-8') | ||
1124 | 71 | except subprocess.CalledProcessError as e: | 92 | except subprocess.CalledProcessError as e: |
1125 | 72 | return 'unrecognized service' not in e.output | 93 | return 'unrecognized service' not in e.output |
1126 | 73 | else: | 94 | else: |
1127 | @@ -96,6 +117,26 @@ | |||
1128 | 96 | return user_info | 117 | return user_info |
1129 | 97 | 118 | ||
1130 | 98 | 119 | ||
1131 | 120 | def add_group(group_name, system_group=False): | ||
1132 | 121 | """Add a group to the system""" | ||
1133 | 122 | try: | ||
1134 | 123 | group_info = grp.getgrnam(group_name) | ||
1135 | 124 | log('group {0} already exists!'.format(group_name)) | ||
1136 | 125 | except KeyError: | ||
1137 | 126 | log('creating group {0}'.format(group_name)) | ||
1138 | 127 | cmd = ['addgroup'] | ||
1139 | 128 | if system_group: | ||
1140 | 129 | cmd.append('--system') | ||
1141 | 130 | else: | ||
1142 | 131 | cmd.extend([ | ||
1143 | 132 | '--group', | ||
1144 | 133 | ]) | ||
1145 | 134 | cmd.append(group_name) | ||
1146 | 135 | subprocess.check_call(cmd) | ||
1147 | 136 | group_info = grp.getgrnam(group_name) | ||
1148 | 137 | return group_info | ||
1149 | 138 | |||
1150 | 139 | |||
1151 | 99 | def add_user_to_group(username, group): | 140 | def add_user_to_group(username, group): |
1152 | 100 | """Add a user to a group""" | 141 | """Add a user to a group""" |
1153 | 101 | cmd = [ | 142 | cmd = [ |
1154 | @@ -115,7 +156,7 @@ | |||
1155 | 115 | cmd.append(from_path) | 156 | cmd.append(from_path) |
1156 | 116 | cmd.append(to_path) | 157 | cmd.append(to_path) |
1157 | 117 | log(" ".join(cmd)) | 158 | log(" ".join(cmd)) |
1159 | 118 | return subprocess.check_output(cmd).strip() | 159 | return subprocess.check_output(cmd).decode('UTF-8').strip() |
1160 | 119 | 160 | ||
1161 | 120 | 161 | ||
1162 | 121 | def symlink(source, destination): | 162 | def symlink(source, destination): |
1163 | @@ -130,28 +171,31 @@ | |||
1164 | 130 | subprocess.check_call(cmd) | 171 | subprocess.check_call(cmd) |
1165 | 131 | 172 | ||
1166 | 132 | 173 | ||
1168 | 133 | def mkdir(path, owner='root', group='root', perms=0555, force=False): | 174 | def mkdir(path, owner='root', group='root', perms=0o555, force=False): |
1169 | 134 | """Create a directory""" | 175 | """Create a directory""" |
1170 | 135 | log("Making dir {} {}:{} {:o}".format(path, owner, group, | 176 | log("Making dir {} {}:{} {:o}".format(path, owner, group, |
1171 | 136 | perms)) | 177 | perms)) |
1172 | 137 | uid = pwd.getpwnam(owner).pw_uid | 178 | uid = pwd.getpwnam(owner).pw_uid |
1173 | 138 | gid = grp.getgrnam(group).gr_gid | 179 | gid = grp.getgrnam(group).gr_gid |
1174 | 139 | realpath = os.path.abspath(path) | 180 | realpath = os.path.abspath(path) |
1177 | 140 | if os.path.exists(realpath): | 181 | path_exists = os.path.exists(realpath) |
1178 | 141 | if force and not os.path.isdir(realpath): | 182 | if path_exists and force: |
1179 | 183 | if not os.path.isdir(realpath): | ||
1180 | 142 | log("Removing non-directory file {} prior to mkdir()".format(path)) | 184 | log("Removing non-directory file {} prior to mkdir()".format(path)) |
1181 | 143 | os.unlink(realpath) | 185 | os.unlink(realpath) |
1183 | 144 | else: | 186 | os.makedirs(realpath, perms) |
1184 | 187 | elif not path_exists: | ||
1185 | 145 | os.makedirs(realpath, perms) | 188 | os.makedirs(realpath, perms) |
1186 | 146 | os.chown(realpath, uid, gid) | 189 | os.chown(realpath, uid, gid) |
1191 | 147 | 190 | os.chmod(realpath, perms) | |
1192 | 148 | 191 | ||
1193 | 149 | def write_file(path, content, owner='root', group='root', perms=0444): | 192 | |
1194 | 150 | """Create or overwrite a file with the contents of a string""" | 193 | def write_file(path, content, owner='root', group='root', perms=0o444): |
1195 | 194 | """Create or overwrite a file with the contents of a byte string.""" | ||
1196 | 151 | log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) | 195 | log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) |
1197 | 152 | uid = pwd.getpwnam(owner).pw_uid | 196 | uid = pwd.getpwnam(owner).pw_uid |
1198 | 153 | gid = grp.getgrnam(group).gr_gid | 197 | gid = grp.getgrnam(group).gr_gid |
1200 | 154 | with open(path, 'w') as target: | 198 | with open(path, 'wb') as target: |
1201 | 155 | os.fchown(target.fileno(), uid, gid) | 199 | os.fchown(target.fileno(), uid, gid) |
1202 | 156 | os.fchmod(target.fileno(), perms) | 200 | os.fchmod(target.fileno(), perms) |
1203 | 157 | target.write(content) | 201 | target.write(content) |
1204 | @@ -177,7 +221,7 @@ | |||
1205 | 177 | cmd_args.extend([device, mountpoint]) | 221 | cmd_args.extend([device, mountpoint]) |
1206 | 178 | try: | 222 | try: |
1207 | 179 | subprocess.check_output(cmd_args) | 223 | subprocess.check_output(cmd_args) |
1209 | 180 | except subprocess.CalledProcessError, e: | 224 | except subprocess.CalledProcessError as e: |
1210 | 181 | log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output)) | 225 | log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output)) |
1211 | 182 | return False | 226 | return False |
1212 | 183 | 227 | ||
1213 | @@ -191,7 +235,7 @@ | |||
1214 | 191 | cmd_args = ['umount', mountpoint] | 235 | cmd_args = ['umount', mountpoint] |
1215 | 192 | try: | 236 | try: |
1216 | 193 | subprocess.check_output(cmd_args) | 237 | subprocess.check_output(cmd_args) |
1218 | 194 | except subprocess.CalledProcessError, e: | 238 | except subprocess.CalledProcessError as e: |
1219 | 195 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) | 239 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) |
1220 | 196 | return False | 240 | return False |
1221 | 197 | 241 | ||
1222 | @@ -218,8 +262,8 @@ | |||
1223 | 218 | """ | 262 | """ |
1224 | 219 | if os.path.exists(path): | 263 | if os.path.exists(path): |
1225 | 220 | h = getattr(hashlib, hash_type)() | 264 | h = getattr(hashlib, hash_type)() |
1228 | 221 | with open(path, 'r') as source: | 265 | with open(path, 'rb') as source: |
1229 | 222 | h.update(source.read()) # IGNORE:E1101 - it does have update | 266 | h.update(source.read()) |
1230 | 223 | return h.hexdigest() | 267 | return h.hexdigest() |
1231 | 224 | else: | 268 | else: |
1232 | 225 | return None | 269 | return None |
1233 | @@ -261,11 +305,11 @@ | |||
1234 | 261 | ceph_client_changed function. | 305 | ceph_client_changed function. |
1235 | 262 | """ | 306 | """ |
1236 | 263 | def wrap(f): | 307 | def wrap(f): |
1238 | 264 | def wrapped_f(*args): | 308 | def wrapped_f(*args, **kwargs): |
1239 | 265 | checksums = {} | 309 | checksums = {} |
1240 | 266 | for path in restart_map: | 310 | for path in restart_map: |
1241 | 267 | checksums[path] = file_hash(path) | 311 | checksums[path] = file_hash(path) |
1243 | 268 | f(*args) | 312 | f(*args, **kwargs) |
1244 | 269 | restarts = [] | 313 | restarts = [] |
1245 | 270 | for path in restart_map: | 314 | for path in restart_map: |
1246 | 271 | if checksums[path] != file_hash(path): | 315 | if checksums[path] != file_hash(path): |
1247 | @@ -295,29 +339,39 @@ | |||
1248 | 295 | def pwgen(length=None): | 339 | def pwgen(length=None): |
1249 | 296 | """Generate a random pasword.""" | 340 | """Generate a random pasword.""" |
1250 | 297 | if length is None: | 341 | if length is None: |
1251 | 342 | # A random length is ok to use a weak PRNG | ||
1252 | 298 | length = random.choice(range(35, 45)) | 343 | length = random.choice(range(35, 45)) |
1253 | 299 | alphanumeric_chars = [ | 344 | alphanumeric_chars = [ |
1255 | 300 | l for l in (string.letters + string.digits) | 345 | l for l in (string.ascii_letters + string.digits) |
1256 | 301 | if l not in 'l0QD1vAEIOUaeiou'] | 346 | if l not in 'l0QD1vAEIOUaeiou'] |
1257 | 347 | # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the | ||
1258 | 348 | # actual password | ||
1259 | 349 | random_generator = random.SystemRandom() | ||
1260 | 302 | random_chars = [ | 350 | random_chars = [ |
1262 | 303 | random.choice(alphanumeric_chars) for _ in range(length)] | 351 | random_generator.choice(alphanumeric_chars) for _ in range(length)] |
1263 | 304 | return(''.join(random_chars)) | 352 | return(''.join(random_chars)) |
1264 | 305 | 353 | ||
1265 | 306 | 354 | ||
1266 | 307 | def list_nics(nic_type): | 355 | def list_nics(nic_type): |
1267 | 308 | '''Return a list of nics of given type(s)''' | 356 | '''Return a list of nics of given type(s)''' |
1269 | 309 | if isinstance(nic_type, basestring): | 357 | if isinstance(nic_type, six.string_types): |
1270 | 310 | int_types = [nic_type] | 358 | int_types = [nic_type] |
1271 | 311 | else: | 359 | else: |
1272 | 312 | int_types = nic_type | 360 | int_types = nic_type |
1273 | 313 | interfaces = [] | 361 | interfaces = [] |
1274 | 314 | for int_type in int_types: | 362 | for int_type in int_types: |
1275 | 315 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] | 363 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
1277 | 316 | ip_output = subprocess.check_output(cmd).split('\n') | 364 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1278 | 317 | ip_output = (line for line in ip_output if line) | 365 | ip_output = (line for line in ip_output if line) |
1279 | 318 | for line in ip_output: | 366 | for line in ip_output: |
1280 | 319 | if line.split()[1].startswith(int_type): | 367 | if line.split()[1].startswith(int_type): |
1282 | 320 | interfaces.append(line.split()[1].replace(":", "")) | 368 | matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line) |
1283 | 369 | if matched: | ||
1284 | 370 | interface = matched.groups()[0] | ||
1285 | 371 | else: | ||
1286 | 372 | interface = line.split()[1].replace(":", "") | ||
1287 | 373 | interfaces.append(interface) | ||
1288 | 374 | |||
1289 | 321 | return interfaces | 375 | return interfaces |
1290 | 322 | 376 | ||
1291 | 323 | 377 | ||
1292 | @@ -329,7 +383,7 @@ | |||
1293 | 329 | 383 | ||
1294 | 330 | def get_nic_mtu(nic): | 384 | def get_nic_mtu(nic): |
1295 | 331 | cmd = ['ip', 'addr', 'show', nic] | 385 | cmd = ['ip', 'addr', 'show', nic] |
1297 | 332 | ip_output = subprocess.check_output(cmd).split('\n') | 386 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1298 | 333 | mtu = "" | 387 | mtu = "" |
1299 | 334 | for line in ip_output: | 388 | for line in ip_output: |
1300 | 335 | words = line.split() | 389 | words = line.split() |
1301 | @@ -340,7 +394,7 @@ | |||
1302 | 340 | 394 | ||
1303 | 341 | def get_nic_hwaddr(nic): | 395 | def get_nic_hwaddr(nic): |
1304 | 342 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] | 396 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] |
1306 | 343 | ip_output = subprocess.check_output(cmd) | 397 | ip_output = subprocess.check_output(cmd).decode('UTF-8') |
1307 | 344 | hwaddr = "" | 398 | hwaddr = "" |
1308 | 345 | words = ip_output.split() | 399 | words = ip_output.split() |
1309 | 346 | if 'link/ether' in words: | 400 | if 'link/ether' in words: |
1310 | @@ -355,10 +409,13 @@ | |||
1311 | 355 | * 0 => Installed revno is the same as supplied arg | 409 | * 0 => Installed revno is the same as supplied arg |
1312 | 356 | * -1 => Installed revno is less than supplied arg | 410 | * -1 => Installed revno is less than supplied arg |
1313 | 357 | 411 | ||
1314 | 412 | This function imports apt_cache function from charmhelpers.fetch if | ||
1315 | 413 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if | ||
1316 | 414 | you call this function, or pass an apt_pkg.Cache() instance. | ||
1317 | 358 | ''' | 415 | ''' |
1318 | 359 | import apt_pkg | 416 | import apt_pkg |
1319 | 360 | from charmhelpers.fetch import apt_cache | ||
1320 | 361 | if not pkgcache: | 417 | if not pkgcache: |
1321 | 418 | from charmhelpers.fetch import apt_cache | ||
1322 | 362 | pkgcache = apt_cache() | 419 | pkgcache = apt_cache() |
1323 | 363 | pkg = pkgcache[package] | 420 | pkg = pkgcache[package] |
1324 | 364 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) | 421 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
1325 | @@ -373,13 +430,21 @@ | |||
1326 | 373 | os.chdir(cur) | 430 | os.chdir(cur) |
1327 | 374 | 431 | ||
1328 | 375 | 432 | ||
1330 | 376 | def chownr(path, owner, group): | 433 | def chownr(path, owner, group, follow_links=True): |
1331 | 377 | uid = pwd.getpwnam(owner).pw_uid | 434 | uid = pwd.getpwnam(owner).pw_uid |
1332 | 378 | gid = grp.getgrnam(group).gr_gid | 435 | gid = grp.getgrnam(group).gr_gid |
1333 | 436 | if follow_links: | ||
1334 | 437 | chown = os.chown | ||
1335 | 438 | else: | ||
1336 | 439 | chown = os.lchown | ||
1337 | 379 | 440 | ||
1338 | 380 | for root, dirs, files in os.walk(path): | 441 | for root, dirs, files in os.walk(path): |
1339 | 381 | for name in dirs + files: | 442 | for name in dirs + files: |
1340 | 382 | full = os.path.join(root, name) | 443 | full = os.path.join(root, name) |
1341 | 383 | broken_symlink = os.path.lexists(full) and not os.path.exists(full) | 444 | broken_symlink = os.path.lexists(full) and not os.path.exists(full) |
1342 | 384 | if not broken_symlink: | 445 | if not broken_symlink: |
1344 | 385 | os.chown(full, uid, gid) | 446 | chown(full, uid, gid) |
1345 | 447 | |||
1346 | 448 | |||
1347 | 449 | def lchownr(path, owner, group): | ||
1348 | 450 | chownr(path, owner, group, follow_links=False) | ||
1349 | 386 | 451 | ||
1350 | === modified file 'hooks/charmhelpers/core/services/__init__.py' | |||
1351 | --- hooks/charmhelpers/core/services/__init__.py 2014-09-26 08:54:54 +0000 | |||
1352 | +++ hooks/charmhelpers/core/services/__init__.py 2015-03-31 04:43:29 +0000 | |||
1353 | @@ -1,2 +1,18 @@ | |||
1356 | 1 | from .base import * | 1 | # Copyright 2014-2015 Canonical Limited. |
1357 | 2 | from .helpers import * | 2 | # |
1358 | 3 | # This file is part of charm-helpers. | ||
1359 | 4 | # | ||
1360 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1361 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1362 | 7 | # published by the Free Software Foundation. | ||
1363 | 8 | # | ||
1364 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1365 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1366 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1367 | 12 | # GNU Lesser General Public License for more details. | ||
1368 | 13 | # | ||
1369 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1370 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1371 | 16 | |||
1372 | 17 | from .base import * # NOQA | ||
1373 | 18 | from .helpers import * # NOQA | ||
1374 | 3 | 19 | ||
1375 | === modified file 'hooks/charmhelpers/core/services/base.py' | |||
1376 | --- hooks/charmhelpers/core/services/base.py 2014-09-26 08:54:54 +0000 | |||
1377 | +++ hooks/charmhelpers/core/services/base.py 2015-03-31 04:43:29 +0000 | |||
1378 | @@ -1,3 +1,19 @@ | |||
1379 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
1380 | 2 | # | ||
1381 | 3 | # This file is part of charm-helpers. | ||
1382 | 4 | # | ||
1383 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1384 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1385 | 7 | # published by the Free Software Foundation. | ||
1386 | 8 | # | ||
1387 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1388 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1389 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1390 | 12 | # GNU Lesser General Public License for more details. | ||
1391 | 13 | # | ||
1392 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1393 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1394 | 16 | |||
1395 | 1 | import os | 17 | import os |
1396 | 2 | import re | 18 | import re |
1397 | 3 | import json | 19 | import json |
1398 | 4 | 20 | ||
1399 | === modified file 'hooks/charmhelpers/core/services/helpers.py' | |||
1400 | --- hooks/charmhelpers/core/services/helpers.py 2014-09-26 08:54:54 +0000 | |||
1401 | +++ hooks/charmhelpers/core/services/helpers.py 2015-03-31 04:43:29 +0000 | |||
1402 | @@ -1,3 +1,19 @@ | |||
1403 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
1404 | 2 | # | ||
1405 | 3 | # This file is part of charm-helpers. | ||
1406 | 4 | # | ||
1407 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1408 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1409 | 7 | # published by the Free Software Foundation. | ||
1410 | 8 | # | ||
1411 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1412 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1413 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1414 | 12 | # GNU Lesser General Public License for more details. | ||
1415 | 13 | # | ||
1416 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1417 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1418 | 16 | |||
1419 | 1 | import os | 17 | import os |
1420 | 2 | import yaml | 18 | import yaml |
1421 | 3 | from charmhelpers.core import hookenv | 19 | from charmhelpers.core import hookenv |
1422 | @@ -29,12 +45,14 @@ | |||
1423 | 29 | """ | 45 | """ |
1424 | 30 | name = None | 46 | name = None |
1425 | 31 | interface = None | 47 | interface = None |
1426 | 32 | required_keys = [] | ||
1427 | 33 | 48 | ||
1428 | 34 | def __init__(self, name=None, additional_required_keys=None): | 49 | def __init__(self, name=None, additional_required_keys=None): |
1429 | 50 | if not hasattr(self, 'required_keys'): | ||
1430 | 51 | self.required_keys = [] | ||
1431 | 52 | |||
1432 | 35 | if name is not None: | 53 | if name is not None: |
1433 | 36 | self.name = name | 54 | self.name = name |
1435 | 37 | if additional_required_keys is not None: | 55 | if additional_required_keys: |
1436 | 38 | self.required_keys.extend(additional_required_keys) | 56 | self.required_keys.extend(additional_required_keys) |
1437 | 39 | self.get_data() | 57 | self.get_data() |
1438 | 40 | 58 | ||
1439 | @@ -118,7 +136,10 @@ | |||
1440 | 118 | """ | 136 | """ |
1441 | 119 | name = 'db' | 137 | name = 'db' |
1442 | 120 | interface = 'mysql' | 138 | interface = 'mysql' |
1444 | 121 | required_keys = ['host', 'user', 'password', 'database'] | 139 | |
1445 | 140 | def __init__(self, *args, **kwargs): | ||
1446 | 141 | self.required_keys = ['host', 'user', 'password', 'database'] | ||
1447 | 142 | super(HttpRelation).__init__(self, *args, **kwargs) | ||
1448 | 122 | 143 | ||
1449 | 123 | 144 | ||
1450 | 124 | class HttpRelation(RelationContext): | 145 | class HttpRelation(RelationContext): |
1451 | @@ -130,7 +151,10 @@ | |||
1452 | 130 | """ | 151 | """ |
1453 | 131 | name = 'website' | 152 | name = 'website' |
1454 | 132 | interface = 'http' | 153 | interface = 'http' |
1456 | 133 | required_keys = ['host', 'port'] | 154 | |
1457 | 155 | def __init__(self, *args, **kwargs): | ||
1458 | 156 | self.required_keys = ['host', 'port'] | ||
1459 | 157 | super(HttpRelation).__init__(self, *args, **kwargs) | ||
1460 | 134 | 158 | ||
1461 | 135 | def provide_data(self): | 159 | def provide_data(self): |
1462 | 136 | return { | 160 | return { |
1463 | @@ -196,7 +220,7 @@ | |||
1464 | 196 | if not os.path.isabs(file_name): | 220 | if not os.path.isabs(file_name): |
1465 | 197 | file_name = os.path.join(hookenv.charm_dir(), file_name) | 221 | file_name = os.path.join(hookenv.charm_dir(), file_name) |
1466 | 198 | with open(file_name, 'w') as file_stream: | 222 | with open(file_name, 'w') as file_stream: |
1468 | 199 | os.fchmod(file_stream.fileno(), 0600) | 223 | os.fchmod(file_stream.fileno(), 0o600) |
1469 | 200 | yaml.dump(config_data, file_stream) | 224 | yaml.dump(config_data, file_stream) |
1470 | 201 | 225 | ||
1471 | 202 | def read_context(self, file_name): | 226 | def read_context(self, file_name): |
1472 | @@ -211,15 +235,19 @@ | |||
1473 | 211 | 235 | ||
1474 | 212 | class TemplateCallback(ManagerCallback): | 236 | class TemplateCallback(ManagerCallback): |
1475 | 213 | """ | 237 | """ |
1479 | 214 | Callback class that will render a Jinja2 template, for use as a ready action. | 238 | Callback class that will render a Jinja2 template, for use as a ready |
1480 | 215 | 239 | action. | |
1481 | 216 | :param str source: The template source file, relative to `$CHARM_DIR/templates` | 240 | |
1482 | 241 | :param str source: The template source file, relative to | ||
1483 | 242 | `$CHARM_DIR/templates` | ||
1484 | 243 | |||
1485 | 217 | :param str target: The target to write the rendered template to | 244 | :param str target: The target to write the rendered template to |
1486 | 218 | :param str owner: The owner of the rendered file | 245 | :param str owner: The owner of the rendered file |
1487 | 219 | :param str group: The group of the rendered file | 246 | :param str group: The group of the rendered file |
1488 | 220 | :param int perms: The permissions of the rendered file | 247 | :param int perms: The permissions of the rendered file |
1489 | 221 | """ | 248 | """ |
1491 | 222 | def __init__(self, source, target, owner='root', group='root', perms=0444): | 249 | def __init__(self, source, target, |
1492 | 250 | owner='root', group='root', perms=0o444): | ||
1493 | 223 | self.source = source | 251 | self.source = source |
1494 | 224 | self.target = target | 252 | self.target = target |
1495 | 225 | self.owner = owner | 253 | self.owner = owner |
1496 | 226 | 254 | ||
1497 | === added file 'hooks/charmhelpers/core/strutils.py' | |||
1498 | --- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000 | |||
1499 | +++ hooks/charmhelpers/core/strutils.py 2015-03-31 04:43:29 +0000 | |||
1500 | @@ -0,0 +1,42 @@ | |||
1501 | 1 | #!/usr/bin/env python | ||
1502 | 2 | # -*- coding: utf-8 -*- | ||
1503 | 3 | |||
1504 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
1505 | 5 | # | ||
1506 | 6 | # This file is part of charm-helpers. | ||
1507 | 7 | # | ||
1508 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1509 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1510 | 10 | # published by the Free Software Foundation. | ||
1511 | 11 | # | ||
1512 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
1513 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1514 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1515 | 15 | # GNU Lesser General Public License for more details. | ||
1516 | 16 | # | ||
1517 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
1518 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1519 | 19 | |||
1520 | 20 | import six | ||
1521 | 21 | |||
1522 | 22 | |||
1523 | 23 | def bool_from_string(value): | ||
1524 | 24 | """Interpret string value as boolean. | ||
1525 | 25 | |||
1526 | 26 | Returns True if value translates to True otherwise False. | ||
1527 | 27 | """ | ||
1528 | 28 | if isinstance(value, six.string_types): | ||
1529 | 29 | value = six.text_type(value) | ||
1530 | 30 | else: | ||
1531 | 31 | msg = "Unable to interpret non-string value '%s' as boolean" % (value) | ||
1532 | 32 | raise ValueError(msg) | ||
1533 | 33 | |||
1534 | 34 | value = value.strip().lower() | ||
1535 | 35 | |||
1536 | 36 | if value in ['y', 'yes', 'true', 't']: | ||
1537 | 37 | return True | ||
1538 | 38 | elif value in ['n', 'no', 'false', 'f']: | ||
1539 | 39 | return False | ||
1540 | 40 | |||
1541 | 41 | msg = "Unable to interpret string value '%s' as boolean" % (value) | ||
1542 | 42 | raise ValueError(msg) | ||
1543 | 0 | 43 | ||
1544 | === added file 'hooks/charmhelpers/core/sysctl.py' | |||
1545 | --- hooks/charmhelpers/core/sysctl.py 1970-01-01 00:00:00 +0000 | |||
1546 | +++ hooks/charmhelpers/core/sysctl.py 2015-03-31 04:43:29 +0000 | |||
1547 | @@ -0,0 +1,56 @@ | |||
1548 | 1 | #!/usr/bin/env python | ||
1549 | 2 | # -*- coding: utf-8 -*- | ||
1550 | 3 | |||
1551 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
1552 | 5 | # | ||
1553 | 6 | # This file is part of charm-helpers. | ||
1554 | 7 | # | ||
1555 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1556 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1557 | 10 | # published by the Free Software Foundation. | ||
1558 | 11 | # | ||
1559 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
1560 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1561 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1562 | 15 | # GNU Lesser General Public License for more details. | ||
1563 | 16 | # | ||
1564 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
1565 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1566 | 19 | |||
1567 | 20 | import yaml | ||
1568 | 21 | |||
1569 | 22 | from subprocess import check_call | ||
1570 | 23 | |||
1571 | 24 | from charmhelpers.core.hookenv import ( | ||
1572 | 25 | log, | ||
1573 | 26 | DEBUG, | ||
1574 | 27 | ERROR, | ||
1575 | 28 | ) | ||
1576 | 29 | |||
1577 | 30 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' | ||
1578 | 31 | |||
1579 | 32 | |||
1580 | 33 | def create(sysctl_dict, sysctl_file): | ||
1581 | 34 | """Creates a sysctl.conf file from a YAML associative array | ||
1582 | 35 | |||
1583 | 36 | :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }" | ||
1584 | 37 | :type sysctl_dict: str | ||
1585 | 38 | :param sysctl_file: path to the sysctl file to be saved | ||
1586 | 39 | :type sysctl_file: str or unicode | ||
1587 | 40 | :returns: None | ||
1588 | 41 | """ | ||
1589 | 42 | try: | ||
1590 | 43 | sysctl_dict_parsed = yaml.safe_load(sysctl_dict) | ||
1591 | 44 | except yaml.YAMLError: | ||
1592 | 45 | log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict), | ||
1593 | 46 | level=ERROR) | ||
1594 | 47 | return | ||
1595 | 48 | |||
1596 | 49 | with open(sysctl_file, "w") as fd: | ||
1597 | 50 | for key, value in sysctl_dict_parsed.items(): | ||
1598 | 51 | fd.write("{}={}\n".format(key, value)) | ||
1599 | 52 | |||
1600 | 53 | log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed), | ||
1601 | 54 | level=DEBUG) | ||
1602 | 55 | |||
1603 | 56 | check_call(["sysctl", "-p", sysctl_file]) | ||
1604 | 0 | 57 | ||
1605 | === modified file 'hooks/charmhelpers/core/templating.py' | |||
1606 | --- hooks/charmhelpers/core/templating.py 2014-09-26 08:54:54 +0000 | |||
1607 | +++ hooks/charmhelpers/core/templating.py 2015-03-31 04:43:29 +0000 | |||
1608 | @@ -1,10 +1,27 @@ | |||
1609 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
1610 | 2 | # | ||
1611 | 3 | # This file is part of charm-helpers. | ||
1612 | 4 | # | ||
1613 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1614 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1615 | 7 | # published by the Free Software Foundation. | ||
1616 | 8 | # | ||
1617 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
1618 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1619 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1620 | 12 | # GNU Lesser General Public License for more details. | ||
1621 | 13 | # | ||
1622 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1623 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1624 | 16 | |||
1625 | 1 | import os | 17 | import os |
1626 | 2 | 18 | ||
1627 | 3 | from charmhelpers.core import host | 19 | from charmhelpers.core import host |
1628 | 4 | from charmhelpers.core import hookenv | 20 | from charmhelpers.core import hookenv |
1629 | 5 | 21 | ||
1630 | 6 | 22 | ||
1632 | 7 | def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None): | 23 | def render(source, target, context, owner='root', group='root', |
1633 | 24 | perms=0o444, templates_dir=None, encoding='UTF-8'): | ||
1634 | 8 | """ | 25 | """ |
1635 | 9 | Render a template. | 26 | Render a template. |
1636 | 10 | 27 | ||
1637 | @@ -47,5 +64,5 @@ | |||
1638 | 47 | level=hookenv.ERROR) | 64 | level=hookenv.ERROR) |
1639 | 48 | raise e | 65 | raise e |
1640 | 49 | content = template.render(context) | 66 | content = template.render(context) |
1643 | 50 | host.mkdir(os.path.dirname(target)) | 67 | host.mkdir(os.path.dirname(target), owner, group, perms=0o755) |
1644 | 51 | host.write_file(target, content, owner, group, perms) | 68 | host.write_file(target, content.encode(encoding), owner, group, perms) |
1645 | 52 | 69 | ||
1646 | === added file 'hooks/charmhelpers/core/unitdata.py' | |||
1647 | --- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000 | |||
1648 | +++ hooks/charmhelpers/core/unitdata.py 2015-03-31 04:43:29 +0000 | |||
1649 | @@ -0,0 +1,477 @@ | |||
1650 | 1 | #!/usr/bin/env python | ||
1651 | 2 | # -*- coding: utf-8 -*- | ||
1652 | 3 | # | ||
1653 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
1654 | 5 | # | ||
1655 | 6 | # This file is part of charm-helpers. | ||
1656 | 7 | # | ||
1657 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | ||
1658 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
1659 | 10 | # published by the Free Software Foundation. | ||
1660 | 11 | # | ||
1661 | 12 | # charm-helpers is distributed in the hope that it will be useful, | ||
1662 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1663 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1664 | 15 | # GNU Lesser General Public License for more details. | ||
1665 | 16 | # | ||
1666 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
1667 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1668 | 19 | # | ||
1669 | 20 | # | ||
1670 | 21 | # Authors: | ||
1671 | 22 | # Kapil Thangavelu <kapil.foss@gmail.com> | ||
1672 | 23 | # | ||
1673 | 24 | """ | ||
1674 | 25 | Intro | ||
1675 | 26 | ----- | ||
1676 | 27 | |||
1677 | 28 | A simple way to store state in units. This provides a key value | ||
1678 | 29 | storage with support for versioned, transactional operation, | ||
1679 | 30 | and can calculate deltas from previous values to simplify unit logic | ||
1680 | 31 | when processing changes. | ||
1681 | 32 | |||
1682 | 33 | |||
1683 | 34 | Hook Integration | ||
1684 | 35 | ---------------- | ||
1685 | 36 | |||
1686 | 37 | There are several extant frameworks for hook execution, including | ||
1687 | 38 | |||
1688 | 39 | - charmhelpers.core.hookenv.Hooks | ||
1689 | 40 | - charmhelpers.core.services.ServiceManager | ||
1690 | 41 | |||
1691 | 42 | The storage classes are framework agnostic, one simple integration is | ||
1692 | 43 | via the HookData contextmanager. It will record the current hook | ||
1693 | 44 | execution environment (including relation data, config data, etc.), | ||
1694 | 45 | setup a transaction and allow easy access to the changes from | ||
1695 | 46 | previously seen values. One consequence of the integration is the | ||
1696 | 47 | reservation of particular keys ('rels', 'unit', 'env', 'config', | ||
1697 | 48 | 'charm_revisions') for their respective values. | ||
1698 | 49 | |||
1699 | 50 | Here's a fully worked integration example using hookenv.Hooks:: | ||
1700 | 51 | |||
1701 | 52 | from charmhelper.core import hookenv, unitdata | ||
1702 | 53 | |||
1703 | 54 | hook_data = unitdata.HookData() | ||
1704 | 55 | db = unitdata.kv() | ||
1705 | 56 | hooks = hookenv.Hooks() | ||
1706 | 57 | |||
1707 | 58 | @hooks.hook | ||
1708 | 59 | def config_changed(): | ||
1709 | 60 | # Print all changes to configuration from previously seen | ||
1710 | 61 | # values. | ||
1711 | 62 | for changed, (prev, cur) in hook_data.conf.items(): | ||
1712 | 63 | print('config changed', changed, | ||
1713 | 64 | 'previous value', prev, | ||
1714 | 65 | 'current value', cur) | ||
1715 | 66 | |||
1716 | 67 | # Get some unit specific bookeeping | ||
1717 | 68 | if not db.get('pkg_key'): | ||
1718 | 69 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
1719 | 70 | db.set('pkg_key', key) | ||
1720 | 71 | |||
1721 | 72 | # Directly access all charm config as a mapping. | ||
1722 | 73 | conf = db.getrange('config', True) | ||
1723 | 74 | |||
1724 | 75 | # Directly access all relation data as a mapping | ||
1725 | 76 | rels = db.getrange('rels', True) | ||
1726 | 77 | |||
1727 | 78 | if __name__ == '__main__': | ||
1728 | 79 | with hook_data(): | ||
1729 | 80 | hook.execute() | ||
1730 | 81 | |||
1731 | 82 | |||
1732 | 83 | A more basic integration is via the hook_scope context manager which simply | ||
1733 | 84 | manages transaction scope (and records hook name, and timestamp):: | ||
1734 | 85 | |||
1735 | 86 | >>> from unitdata import kv | ||
1736 | 87 | >>> db = kv() | ||
1737 | 88 | >>> with db.hook_scope('install'): | ||
1738 | 89 | ... # do work, in transactional scope. | ||
1739 | 90 | ... db.set('x', 1) | ||
1740 | 91 | >>> db.get('x') | ||
1741 | 92 | 1 | ||
1742 | 93 | |||
1743 | 94 | |||
1744 | 95 | Usage | ||
1745 | 96 | ----- | ||
1746 | 97 | |||
1747 | 98 | Values are automatically json de/serialized to preserve basic typing | ||
1748 | 99 | and complex data struct capabilities (dicts, lists, ints, booleans, etc). | ||
1749 | 100 | |||
1750 | 101 | Individual values can be manipulated via get/set:: | ||
1751 | 102 | |||
1752 | 103 | >>> kv.set('y', True) | ||
1753 | 104 | >>> kv.get('y') | ||
1754 | 105 | True | ||
1755 | 106 | |||
1756 | 107 | # We can set complex values (dicts, lists) as a single key. | ||
1757 | 108 | >>> kv.set('config', {'a': 1, 'b': True'}) | ||
1758 | 109 | |||
1759 | 110 | # Also supports returning dictionaries as a record which | ||
1760 | 111 | # provides attribute access. | ||
1761 | 112 | >>> config = kv.get('config', record=True) | ||
1762 | 113 | >>> config.b | ||
1763 | 114 | True | ||
1764 | 115 | |||
1765 | 116 | |||
1766 | 117 | Groups of keys can be manipulated with update/getrange:: | ||
1767 | 118 | |||
1768 | 119 | >>> kv.update({'z': 1, 'y': 2}, prefix="gui.") | ||
1769 | 120 | >>> kv.getrange('gui.', strip=True) | ||
1770 | 121 | {'z': 1, 'y': 2} | ||
1771 | 122 | |||
1772 | 123 | When updating values, its very helpful to understand which values | ||
1773 | 124 | have actually changed and how have they changed. The storage | ||
1774 | 125 | provides a delta method to provide for this:: | ||
1775 | 126 | |||
1776 | 127 | >>> data = {'debug': True, 'option': 2} | ||
1777 | 128 | >>> delta = kv.delta(data, 'config.') | ||
1778 | 129 | >>> delta.debug.previous | ||
1779 | 130 | None | ||
1780 | 131 | >>> delta.debug.current | ||
1781 | 132 | True | ||
1782 | 133 | >>> delta | ||
1783 | 134 | {'debug': (None, True), 'option': (None, 2)} | ||
1784 | 135 | |||
1785 | 136 | Note the delta method does not persist the actual change, it needs to | ||
1786 | 137 | be explicitly saved via 'update' method:: | ||
1787 | 138 | |||
1788 | 139 | >>> kv.update(data, 'config.') | ||
1789 | 140 | |||
1790 | 141 | Values modified in the context of a hook scope retain historical values | ||
1791 | 142 | associated to the hookname. | ||
1792 | 143 | |||
1793 | 144 | >>> with db.hook_scope('config-changed'): | ||
1794 | 145 | ... db.set('x', 42) | ||
1795 | 146 | >>> db.gethistory('x') | ||
1796 | 147 | [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'), | ||
1797 | 148 | (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')] | ||
1798 | 149 | |||
1799 | 150 | """ | ||
1800 | 151 | |||
1801 | 152 | import collections | ||
1802 | 153 | import contextlib | ||
1803 | 154 | import datetime | ||
1804 | 155 | import json | ||
1805 | 156 | import os | ||
1806 | 157 | import pprint | ||
1807 | 158 | import sqlite3 | ||
1808 | 159 | import sys | ||
1809 | 160 | |||
1810 | 161 | __author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>' | ||
1811 | 162 | |||
1812 | 163 | |||
1813 | 164 | class Storage(object): | ||
1814 | 165 | """Simple key value database for local unit state within charms. | ||
1815 | 166 | |||
1816 | 167 | Modifications are automatically committed at hook exit. That's | ||
1817 | 168 | currently regardless of exit code. | ||
1818 | 169 | |||
1819 | 170 | To support dicts, lists, integer, floats, and booleans values | ||
1820 | 171 | are automatically json encoded/decoded. | ||
1821 | 172 | """ | ||
1822 | 173 | def __init__(self, path=None): | ||
1823 | 174 | self.db_path = path | ||
1824 | 175 | if path is None: | ||
1825 | 176 | self.db_path = os.path.join( | ||
1826 | 177 | os.environ.get('CHARM_DIR', ''), '.unit-state.db') | ||
1827 | 178 | self.conn = sqlite3.connect('%s' % self.db_path) | ||
1828 | 179 | self.cursor = self.conn.cursor() | ||
1829 | 180 | self.revision = None | ||
1830 | 181 | self._closed = False | ||
1831 | 182 | self._init() | ||
1832 | 183 | |||
1833 | 184 | def close(self): | ||
1834 | 185 | if self._closed: | ||
1835 | 186 | return | ||
1836 | 187 | self.flush(False) | ||
1837 | 188 | self.cursor.close() | ||
1838 | 189 | self.conn.close() | ||
1839 | 190 | self._closed = True | ||
1840 | 191 | |||
1841 | 192 | def _scoped_query(self, stmt, params=None): | ||
1842 | 193 | if params is None: | ||
1843 | 194 | params = [] | ||
1844 | 195 | return stmt, params | ||
1845 | 196 | |||
1846 | 197 | def get(self, key, default=None, record=False): | ||
1847 | 198 | self.cursor.execute( | ||
1848 | 199 | *self._scoped_query( | ||
1849 | 200 | 'select data from kv where key=?', [key])) | ||
1850 | 201 | result = self.cursor.fetchone() | ||
1851 | 202 | if not result: | ||
1852 | 203 | return default | ||
1853 | 204 | if record: | ||
1854 | 205 | return Record(json.loads(result[0])) | ||
1855 | 206 | return json.loads(result[0]) | ||
1856 | 207 | |||
1857 | 208 | def getrange(self, key_prefix, strip=False): | ||
1858 | 209 | stmt = "select key, data from kv where key like '%s%%'" % key_prefix | ||
1859 | 210 | self.cursor.execute(*self._scoped_query(stmt)) | ||
1860 | 211 | result = self.cursor.fetchall() | ||
1861 | 212 | |||
1862 | 213 | if not result: | ||
1863 | 214 | return None | ||
1864 | 215 | if not strip: | ||
1865 | 216 | key_prefix = '' | ||
1866 | 217 | return dict([ | ||
1867 | 218 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) | ||
1868 | 219 | |||
1869 | 220 | def update(self, mapping, prefix=""): | ||
1870 | 221 | for k, v in mapping.items(): | ||
1871 | 222 | self.set("%s%s" % (prefix, k), v) | ||
1872 | 223 | |||
1873 | 224 | def unset(self, key): | ||
1874 | 225 | self.cursor.execute('delete from kv where key=?', [key]) | ||
1875 | 226 | if self.revision and self.cursor.rowcount: | ||
1876 | 227 | self.cursor.execute( | ||
1877 | 228 | 'insert into kv_revisions values (?, ?, ?)', | ||
1878 | 229 | [key, self.revision, json.dumps('DELETED')]) | ||
1879 | 230 | |||
1880 | 231 | def set(self, key, value): | ||
1881 | 232 | serialized = json.dumps(value) | ||
1882 | 233 | |||
1883 | 234 | self.cursor.execute( | ||
1884 | 235 | 'select data from kv where key=?', [key]) | ||
1885 | 236 | exists = self.cursor.fetchone() | ||
1886 | 237 | |||
1887 | 238 | # Skip mutations to the same value | ||
1888 | 239 | if exists: | ||
1889 | 240 | if exists[0] == serialized: | ||
1890 | 241 | return value | ||
1891 | 242 | |||
1892 | 243 | if not exists: | ||
1893 | 244 | self.cursor.execute( | ||
1894 | 245 | 'insert into kv (key, data) values (?, ?)', | ||
1895 | 246 | (key, serialized)) | ||
1896 | 247 | else: | ||
1897 | 248 | self.cursor.execute(''' | ||
1898 | 249 | update kv | ||
1899 | 250 | set data = ? | ||
1900 | 251 | where key = ?''', [serialized, key]) | ||
1901 | 252 | |||
1902 | 253 | # Save | ||
1903 | 254 | if not self.revision: | ||
1904 | 255 | return value | ||
1905 | 256 | |||
1906 | 257 | self.cursor.execute( | ||
1907 | 258 | 'select 1 from kv_revisions where key=? and revision=?', | ||
1908 | 259 | [key, self.revision]) | ||
1909 | 260 | exists = self.cursor.fetchone() | ||
1910 | 261 | |||
1911 | 262 | if not exists: | ||
1912 | 263 | self.cursor.execute( | ||
1913 | 264 | '''insert into kv_revisions ( | ||
1914 | 265 | revision, key, data) values (?, ?, ?)''', | ||
1915 | 266 | (self.revision, key, serialized)) | ||
1916 | 267 | else: | ||
1917 | 268 | self.cursor.execute( | ||
1918 | 269 | ''' | ||
1919 | 270 | update kv_revisions | ||
1920 | 271 | set data = ? | ||
1921 | 272 | where key = ? | ||
1922 | 273 | and revision = ?''', | ||
1923 | 274 | [serialized, key, self.revision]) | ||
1924 | 275 | |||
1925 | 276 | return value | ||
1926 | 277 | |||
1927 | 278 | def delta(self, mapping, prefix): | ||
1928 | 279 | """ | ||
1929 | 280 | return a delta containing values that have changed. | ||
1930 | 281 | """ | ||
1931 | 282 | previous = self.getrange(prefix, strip=True) | ||
1932 | 283 | if not previous: | ||
1933 | 284 | pk = set() | ||
1934 | 285 | else: | ||
1935 | 286 | pk = set(previous.keys()) | ||
1936 | 287 | ck = set(mapping.keys()) | ||
1937 | 288 | delta = DeltaSet() | ||
1938 | 289 | |||
1939 | 290 | # added | ||
1940 | 291 | for k in ck.difference(pk): | ||
1941 | 292 | delta[k] = Delta(None, mapping[k]) | ||
1942 | 293 | |||
1943 | 294 | # removed | ||
1944 | 295 | for k in pk.difference(ck): | ||
1945 | 296 | delta[k] = Delta(previous[k], None) | ||
1946 | 297 | |||
1947 | 298 | # changed | ||
1948 | 299 | for k in pk.intersection(ck): | ||
1949 | 300 | c = mapping[k] | ||
1950 | 301 | p = previous[k] | ||
1951 | 302 | if c != p: | ||
1952 | 303 | delta[k] = Delta(p, c) | ||
1953 | 304 | |||
1954 | 305 | return delta | ||
1955 | 306 | |||
1956 | 307 | @contextlib.contextmanager | ||
1957 | 308 | def hook_scope(self, name=""): | ||
1958 | 309 | """Scope all future interactions to the current hook execution | ||
1959 | 310 | revision.""" | ||
1960 | 311 | assert not self.revision | ||
1961 | 312 | self.cursor.execute( | ||
1962 | 313 | 'insert into hooks (hook, date) values (?, ?)', | ||
1963 | 314 | (name or sys.argv[0], | ||
1964 | 315 | datetime.datetime.utcnow().isoformat())) | ||
1965 | 316 | self.revision = self.cursor.lastrowid | ||
1966 | 317 | try: | ||
1967 | 318 | yield self.revision | ||
1968 | 319 | self.revision = None | ||
1969 | 320 | except: | ||
1970 | 321 | self.flush(False) | ||
1971 | 322 | self.revision = None | ||
1972 | 323 | raise | ||
1973 | 324 | else: | ||
1974 | 325 | self.flush() | ||
1975 | 326 | |||
1976 | 327 | def flush(self, save=True): | ||
1977 | 328 | if save: | ||
1978 | 329 | self.conn.commit() | ||
1979 | 330 | elif self._closed: | ||
1980 | 331 | return | ||
1981 | 332 | else: | ||
1982 | 333 | self.conn.rollback() | ||
1983 | 334 | |||
1984 | 335 | def _init(self): | ||
1985 | 336 | self.cursor.execute(''' | ||
1986 | 337 | create table if not exists kv ( | ||
1987 | 338 | key text, | ||
1988 | 339 | data text, | ||
1989 | 340 | primary key (key) | ||
1990 | 341 | )''') | ||
1991 | 342 | self.cursor.execute(''' | ||
1992 | 343 | create table if not exists kv_revisions ( | ||
1993 | 344 | key text, | ||
1994 | 345 | revision integer, | ||
1995 | 346 | data text, | ||
1996 | 347 | primary key (key, revision) | ||
1997 | 348 | )''') | ||
1998 | 349 | self.cursor.execute(''' | ||
1999 | 350 | create table if not exists hooks ( | ||
2000 | 351 | version integer primary key autoincrement, | ||
2001 | 352 | hook text, | ||
2002 | 353 | date text | ||
2003 | 354 | )''') | ||
2004 | 355 | self.conn.commit() | ||
2005 | 356 | |||
2006 | 357 | def gethistory(self, key, deserialize=False): | ||
2007 | 358 | self.cursor.execute( | ||
2008 | 359 | ''' | ||
2009 | 360 | select kv.revision, kv.key, kv.data, h.hook, h.date | ||
2010 | 361 | from kv_revisions kv, | ||
2011 | 362 | hooks h | ||
2012 | 363 | where kv.key=? | ||
2013 | 364 | and kv.revision = h.version | ||
2014 | 365 | ''', [key]) | ||
2015 | 366 | if deserialize is False: | ||
2016 | 367 | return self.cursor.fetchall() | ||
2017 | 368 | return map(_parse_history, self.cursor.fetchall()) | ||
2018 | 369 | |||
2019 | 370 | def debug(self, fh=sys.stderr): | ||
2020 | 371 | self.cursor.execute('select * from kv') | ||
2021 | 372 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
2022 | 373 | self.cursor.execute('select * from kv_revisions') | ||
2023 | 374 | pprint.pprint(self.cursor.fetchall(), stream=fh) | ||
2024 | 375 | |||
2025 | 376 | |||
2026 | 377 | def _parse_history(d): | ||
2027 | 378 | return (d[0], d[1], json.loads(d[2]), d[3], | ||
2028 | 379 | datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f")) | ||
2029 | 380 | |||
2030 | 381 | |||
2031 | 382 | class HookData(object): | ||
2032 | 383 | """Simple integration for existing hook exec frameworks. | ||
2033 | 384 | |||
2034 | 385 | Records all unit information, and stores deltas for processing | ||
2035 | 386 | by the hook. | ||
2036 | 387 | |||
2037 | 388 | Sample:: | ||
2038 | 389 | |||
2039 | 390 | from charmhelper.core import hookenv, unitdata | ||
2040 | 391 | |||
2041 | 392 | changes = unitdata.HookData() | ||
2042 | 393 | db = unitdata.kv() | ||
2043 | 394 | hooks = hookenv.Hooks() | ||
2044 | 395 | |||
2045 | 396 | @hooks.hook | ||
2046 | 397 | def config_changed(): | ||
2047 | 398 | # View all changes to configuration | ||
2048 | 399 | for changed, (prev, cur) in changes.conf.items(): | ||
2049 | 400 | print('config changed', changed, | ||
2050 | 401 | 'previous value', prev, | ||
2051 | 402 | 'current value', cur) | ||
2052 | 403 | |||
2053 | 404 | # Get some unit specific bookeeping | ||
2054 | 405 | if not db.get('pkg_key'): | ||
2055 | 406 | key = urllib.urlopen('https://example.com/pkg_key').read() | ||
2056 | 407 | db.set('pkg_key', key) | ||
2057 | 408 | |||
2058 | 409 | if __name__ == '__main__': | ||
2059 | 410 | with changes(): | ||
2060 | 411 | hook.execute() | ||
2061 | 412 | |||
2062 | 413 | """ | ||
2063 | 414 | def __init__(self): | ||
2064 | 415 | self.kv = kv() | ||
2065 | 416 | self.conf = None | ||
2066 | 417 | self.rels = None | ||
2067 | 418 | |||
2068 | 419 | @contextlib.contextmanager | ||
2069 | 420 | def __call__(self): | ||
2070 | 421 | from charmhelpers.core import hookenv | ||
2071 | 422 | hook_name = hookenv.hook_name() | ||
2072 | 423 | |||
2073 | 424 | with self.kv.hook_scope(hook_name): | ||
2074 | 425 | self._record_charm_version(hookenv.charm_dir()) | ||
2075 | 426 | delta_config, delta_relation = self._record_hook(hookenv) | ||
2076 | 427 | yield self.kv, delta_config, delta_relation | ||
2077 | 428 | |||
2078 | 429 | def _record_charm_version(self, charm_dir): | ||
2079 | 430 | # Record revisions.. charm revisions are meaningless | ||
2080 | 431 | # to charm authors as they don't control the revision. | ||
2081 | 432 | # so logic dependnent on revision is not particularly | ||
2082 | 433 | # useful, however it is useful for debugging analysis. | ||
2083 | 434 | charm_rev = open( | ||
2084 | 435 | os.path.join(charm_dir, 'revision')).read().strip() | ||
2085 | 436 | charm_rev = charm_rev or '0' | ||
2086 | 437 | revs = self.kv.get('charm_revisions', []) | ||
2087 | 438 | if charm_rev not in revs: | ||
2088 | 439 | revs.append(charm_rev.strip() or '0') | ||
2089 | 440 | self.kv.set('charm_revisions', revs) | ||
2090 | 441 | |||
2091 | 442 | def _record_hook(self, hookenv): | ||
2092 | 443 | data = hookenv.execution_environment() | ||
2093 | 444 | self.conf = conf_delta = self.kv.delta(data['conf'], 'config') | ||
2094 | 445 | self.rels = rels_delta = self.kv.delta(data['rels'], 'rels') | ||
2095 | 446 | self.kv.set('env', data['env']) | ||
2096 | 447 | self.kv.set('unit', data['unit']) | ||
2097 | 448 | self.kv.set('relid', data.get('relid')) | ||
2098 | 449 | return conf_delta, rels_delta | ||
2099 | 450 | |||
2100 | 451 | |||
2101 | 452 | class Record(dict): | ||
2102 | 453 | |||
2103 | 454 | __slots__ = () | ||
2104 | 455 | |||
2105 | 456 | def __getattr__(self, k): | ||
2106 | 457 | if k in self: | ||
2107 | 458 | return self[k] | ||
2108 | 459 | raise AttributeError(k) | ||
2109 | 460 | |||
2110 | 461 | |||
2111 | 462 | class DeltaSet(Record): | ||
2112 | 463 | |||
2113 | 464 | __slots__ = () | ||
2114 | 465 | |||
2115 | 466 | |||
2116 | 467 | Delta = collections.namedtuple('Delta', ['previous', 'current']) | ||
2117 | 468 | |||
2118 | 469 | |||
2119 | 470 | _KV = None | ||
2120 | 471 | |||
2121 | 472 | |||
2122 | 473 | def kv(): | ||
2123 | 474 | global _KV | ||
2124 | 475 | if _KV is None: | ||
2125 | 476 | _KV = Storage() | ||
2126 | 477 | return _KV | ||
2127 | 0 | 478 | ||
2128 | === modified file 'hooks/charmhelpers/fetch/__init__.py' | |||
2129 | --- hooks/charmhelpers/fetch/__init__.py 2014-09-26 08:54:54 +0000 | |||
2130 | +++ hooks/charmhelpers/fetch/__init__.py 2015-03-31 04:43:29 +0000 | |||
2131 | @@ -1,3 +1,19 @@ | |||
2132 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2133 | 2 | # | ||
2134 | 3 | # This file is part of charm-helpers. | ||
2135 | 4 | # | ||
2136 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2137 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2138 | 7 | # published by the Free Software Foundation. | ||
2139 | 8 | # | ||
2140 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2141 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2142 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2143 | 12 | # GNU Lesser General Public License for more details. | ||
2144 | 13 | # | ||
2145 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2146 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2147 | 16 | |||
2148 | 1 | import importlib | 17 | import importlib |
2149 | 2 | from tempfile import NamedTemporaryFile | 18 | from tempfile import NamedTemporaryFile |
2150 | 3 | import time | 19 | import time |
2151 | @@ -5,10 +21,6 @@ | |||
2152 | 5 | from charmhelpers.core.host import ( | 21 | from charmhelpers.core.host import ( |
2153 | 6 | lsb_release | 22 | lsb_release |
2154 | 7 | ) | 23 | ) |
2155 | 8 | from urlparse import ( | ||
2156 | 9 | urlparse, | ||
2157 | 10 | urlunparse, | ||
2158 | 11 | ) | ||
2159 | 12 | import subprocess | 24 | import subprocess |
2160 | 13 | from charmhelpers.core.hookenv import ( | 25 | from charmhelpers.core.hookenv import ( |
2161 | 14 | config, | 26 | config, |
2162 | @@ -16,6 +28,12 @@ | |||
2163 | 16 | ) | 28 | ) |
2164 | 17 | import os | 29 | import os |
2165 | 18 | 30 | ||
2166 | 31 | import six | ||
2167 | 32 | if six.PY3: | ||
2168 | 33 | from urllib.parse import urlparse, urlunparse | ||
2169 | 34 | else: | ||
2170 | 35 | from urlparse import urlparse, urlunparse | ||
2171 | 36 | |||
2172 | 19 | 37 | ||
2173 | 20 | CLOUD_ARCHIVE = """# Ubuntu Cloud Archive | 38 | CLOUD_ARCHIVE = """# Ubuntu Cloud Archive |
2174 | 21 | deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main | 39 | deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main |
2175 | @@ -62,9 +80,16 @@ | |||
2176 | 62 | 'trusty-juno/updates': 'trusty-updates/juno', | 80 | 'trusty-juno/updates': 'trusty-updates/juno', |
2177 | 63 | 'trusty-updates/juno': 'trusty-updates/juno', | 81 | 'trusty-updates/juno': 'trusty-updates/juno', |
2178 | 64 | 'juno/proposed': 'trusty-proposed/juno', | 82 | 'juno/proposed': 'trusty-proposed/juno', |
2179 | 65 | 'juno/proposed': 'trusty-proposed/juno', | ||
2180 | 66 | 'trusty-juno/proposed': 'trusty-proposed/juno', | 83 | 'trusty-juno/proposed': 'trusty-proposed/juno', |
2181 | 67 | 'trusty-proposed/juno': 'trusty-proposed/juno', | 84 | 'trusty-proposed/juno': 'trusty-proposed/juno', |
2182 | 85 | # Kilo | ||
2183 | 86 | 'kilo': 'trusty-updates/kilo', | ||
2184 | 87 | 'trusty-kilo': 'trusty-updates/kilo', | ||
2185 | 88 | 'trusty-kilo/updates': 'trusty-updates/kilo', | ||
2186 | 89 | 'trusty-updates/kilo': 'trusty-updates/kilo', | ||
2187 | 90 | 'kilo/proposed': 'trusty-proposed/kilo', | ||
2188 | 91 | 'trusty-kilo/proposed': 'trusty-proposed/kilo', | ||
2189 | 92 | 'trusty-proposed/kilo': 'trusty-proposed/kilo', | ||
2190 | 68 | } | 93 | } |
2191 | 69 | 94 | ||
2192 | 70 | # The order of this list is very important. Handlers should be listed in from | 95 | # The order of this list is very important. Handlers should be listed in from |
2193 | @@ -72,6 +97,7 @@ | |||
2194 | 72 | FETCH_HANDLERS = ( | 97 | FETCH_HANDLERS = ( |
2195 | 73 | 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler', | 98 | 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler', |
2196 | 74 | 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler', | 99 | 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler', |
2197 | 100 | 'charmhelpers.fetch.giturl.GitUrlFetchHandler', | ||
2198 | 75 | ) | 101 | ) |
2199 | 76 | 102 | ||
2200 | 77 | APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. | 103 | APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. |
2201 | @@ -148,7 +174,7 @@ | |||
2202 | 148 | cmd = ['apt-get', '--assume-yes'] | 174 | cmd = ['apt-get', '--assume-yes'] |
2203 | 149 | cmd.extend(options) | 175 | cmd.extend(options) |
2204 | 150 | cmd.append('install') | 176 | cmd.append('install') |
2206 | 151 | if isinstance(packages, basestring): | 177 | if isinstance(packages, six.string_types): |
2207 | 152 | cmd.append(packages) | 178 | cmd.append(packages) |
2208 | 153 | else: | 179 | else: |
2209 | 154 | cmd.extend(packages) | 180 | cmd.extend(packages) |
2210 | @@ -181,7 +207,7 @@ | |||
2211 | 181 | def apt_purge(packages, fatal=False): | 207 | def apt_purge(packages, fatal=False): |
2212 | 182 | """Purge one or more packages""" | 208 | """Purge one or more packages""" |
2213 | 183 | cmd = ['apt-get', '--assume-yes', 'purge'] | 209 | cmd = ['apt-get', '--assume-yes', 'purge'] |
2215 | 184 | if isinstance(packages, basestring): | 210 | if isinstance(packages, six.string_types): |
2216 | 185 | cmd.append(packages) | 211 | cmd.append(packages) |
2217 | 186 | else: | 212 | else: |
2218 | 187 | cmd.extend(packages) | 213 | cmd.extend(packages) |
2219 | @@ -192,7 +218,7 @@ | |||
2220 | 192 | def apt_hold(packages, fatal=False): | 218 | def apt_hold(packages, fatal=False): |
2221 | 193 | """Hold one or more packages""" | 219 | """Hold one or more packages""" |
2222 | 194 | cmd = ['apt-mark', 'hold'] | 220 | cmd = ['apt-mark', 'hold'] |
2224 | 195 | if isinstance(packages, basestring): | 221 | if isinstance(packages, six.string_types): |
2225 | 196 | cmd.append(packages) | 222 | cmd.append(packages) |
2226 | 197 | else: | 223 | else: |
2227 | 198 | cmd.extend(packages) | 224 | cmd.extend(packages) |
2228 | @@ -218,6 +244,7 @@ | |||
2229 | 218 | pocket for the release. | 244 | pocket for the release. |
2230 | 219 | 'cloud:' may be used to activate official cloud archive pockets, | 245 | 'cloud:' may be used to activate official cloud archive pockets, |
2231 | 220 | such as 'cloud:icehouse' | 246 | such as 'cloud:icehouse' |
2232 | 247 | 'distro' may be used as a noop | ||
2233 | 221 | 248 | ||
2234 | 222 | @param key: A key to be added to the system's APT keyring and used | 249 | @param key: A key to be added to the system's APT keyring and used |
2235 | 223 | to verify the signatures on packages. Ideally, this should be an | 250 | to verify the signatures on packages. Ideally, this should be an |
2236 | @@ -251,12 +278,14 @@ | |||
2237 | 251 | release = lsb_release()['DISTRIB_CODENAME'] | 278 | release = lsb_release()['DISTRIB_CODENAME'] |
2238 | 252 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: | 279 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: |
2239 | 253 | apt.write(PROPOSED_POCKET.format(release)) | 280 | apt.write(PROPOSED_POCKET.format(release)) |
2240 | 281 | elif source == 'distro': | ||
2241 | 282 | pass | ||
2242 | 254 | else: | 283 | else: |
2244 | 255 | raise SourceConfigError("Unknown source: {!r}".format(source)) | 284 | log("Unknown source: {!r}".format(source)) |
2245 | 256 | 285 | ||
2246 | 257 | if key: | 286 | if key: |
2247 | 258 | if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: | 287 | if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: |
2249 | 259 | with NamedTemporaryFile() as key_file: | 288 | with NamedTemporaryFile('w+') as key_file: |
2250 | 260 | key_file.write(key) | 289 | key_file.write(key) |
2251 | 261 | key_file.flush() | 290 | key_file.flush() |
2252 | 262 | key_file.seek(0) | 291 | key_file.seek(0) |
2253 | @@ -293,14 +322,14 @@ | |||
2254 | 293 | sources = safe_load((config(sources_var) or '').strip()) or [] | 322 | sources = safe_load((config(sources_var) or '').strip()) or [] |
2255 | 294 | keys = safe_load((config(keys_var) or '').strip()) or None | 323 | keys = safe_load((config(keys_var) or '').strip()) or None |
2256 | 295 | 324 | ||
2258 | 296 | if isinstance(sources, basestring): | 325 | if isinstance(sources, six.string_types): |
2259 | 297 | sources = [sources] | 326 | sources = [sources] |
2260 | 298 | 327 | ||
2261 | 299 | if keys is None: | 328 | if keys is None: |
2262 | 300 | for source in sources: | 329 | for source in sources: |
2263 | 301 | add_source(source, None) | 330 | add_source(source, None) |
2264 | 302 | else: | 331 | else: |
2266 | 303 | if isinstance(keys, basestring): | 332 | if isinstance(keys, six.string_types): |
2267 | 304 | keys = [keys] | 333 | keys = [keys] |
2268 | 305 | 334 | ||
2269 | 306 | if len(sources) != len(keys): | 335 | if len(sources) != len(keys): |
2270 | @@ -397,7 +426,7 @@ | |||
2271 | 397 | while result is None or result == APT_NO_LOCK: | 426 | while result is None or result == APT_NO_LOCK: |
2272 | 398 | try: | 427 | try: |
2273 | 399 | result = subprocess.check_call(cmd, env=env) | 428 | result = subprocess.check_call(cmd, env=env) |
2275 | 400 | except subprocess.CalledProcessError, e: | 429 | except subprocess.CalledProcessError as e: |
2276 | 401 | retry_count = retry_count + 1 | 430 | retry_count = retry_count + 1 |
2277 | 402 | if retry_count > APT_NO_LOCK_RETRY_COUNT: | 431 | if retry_count > APT_NO_LOCK_RETRY_COUNT: |
2278 | 403 | raise | 432 | raise |
2279 | 404 | 433 | ||
2280 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' | |||
2281 | --- hooks/charmhelpers/fetch/archiveurl.py 2014-09-26 08:54:54 +0000 | |||
2282 | +++ hooks/charmhelpers/fetch/archiveurl.py 2015-03-31 04:43:29 +0000 | |||
2283 | @@ -1,8 +1,22 @@ | |||
2284 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2285 | 2 | # | ||
2286 | 3 | # This file is part of charm-helpers. | ||
2287 | 4 | # | ||
2288 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2289 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2290 | 7 | # published by the Free Software Foundation. | ||
2291 | 8 | # | ||
2292 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2293 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2294 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2295 | 12 | # GNU Lesser General Public License for more details. | ||
2296 | 13 | # | ||
2297 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2298 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2299 | 16 | |||
2300 | 1 | import os | 17 | import os |
2301 | 2 | import urllib2 | ||
2302 | 3 | from urllib import urlretrieve | ||
2303 | 4 | import urlparse | ||
2304 | 5 | import hashlib | 18 | import hashlib |
2305 | 19 | import re | ||
2306 | 6 | 20 | ||
2307 | 7 | from charmhelpers.fetch import ( | 21 | from charmhelpers.fetch import ( |
2308 | 8 | BaseFetchHandler, | 22 | BaseFetchHandler, |
2309 | @@ -14,6 +28,41 @@ | |||
2310 | 14 | ) | 28 | ) |
2311 | 15 | from charmhelpers.core.host import mkdir, check_hash | 29 | from charmhelpers.core.host import mkdir, check_hash |
2312 | 16 | 30 | ||
2313 | 31 | import six | ||
2314 | 32 | if six.PY3: | ||
2315 | 33 | from urllib.request import ( | ||
2316 | 34 | build_opener, install_opener, urlopen, urlretrieve, | ||
2317 | 35 | HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, | ||
2318 | 36 | ) | ||
2319 | 37 | from urllib.parse import urlparse, urlunparse, parse_qs | ||
2320 | 38 | from urllib.error import URLError | ||
2321 | 39 | else: | ||
2322 | 40 | from urllib import urlretrieve | ||
2323 | 41 | from urllib2 import ( | ||
2324 | 42 | build_opener, install_opener, urlopen, | ||
2325 | 43 | HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, | ||
2326 | 44 | URLError | ||
2327 | 45 | ) | ||
2328 | 46 | from urlparse import urlparse, urlunparse, parse_qs | ||
2329 | 47 | |||
2330 | 48 | |||
2331 | 49 | def splituser(host): | ||
2332 | 50 | '''urllib.splituser(), but six's support of this seems broken''' | ||
2333 | 51 | _userprog = re.compile('^(.*)@(.*)$') | ||
2334 | 52 | match = _userprog.match(host) | ||
2335 | 53 | if match: | ||
2336 | 54 | return match.group(1, 2) | ||
2337 | 55 | return None, host | ||
2338 | 56 | |||
2339 | 57 | |||
2340 | 58 | def splitpasswd(user): | ||
2341 | 59 | '''urllib.splitpasswd(), but six's support of this is missing''' | ||
2342 | 60 | _passwdprog = re.compile('^([^:]*):(.*)$', re.S) | ||
2343 | 61 | match = _passwdprog.match(user) | ||
2344 | 62 | if match: | ||
2345 | 63 | return match.group(1, 2) | ||
2346 | 64 | return user, None | ||
2347 | 65 | |||
2348 | 17 | 66 | ||
2349 | 18 | class ArchiveUrlFetchHandler(BaseFetchHandler): | 67 | class ArchiveUrlFetchHandler(BaseFetchHandler): |
2350 | 19 | """ | 68 | """ |
2351 | @@ -42,20 +91,20 @@ | |||
2352 | 42 | """ | 91 | """ |
2353 | 43 | # propogate all exceptions | 92 | # propogate all exceptions |
2354 | 44 | # URLError, OSError, etc | 93 | # URLError, OSError, etc |
2356 | 45 | proto, netloc, path, params, query, fragment = urlparse.urlparse(source) | 94 | proto, netloc, path, params, query, fragment = urlparse(source) |
2357 | 46 | if proto in ('http', 'https'): | 95 | if proto in ('http', 'https'): |
2359 | 47 | auth, barehost = urllib2.splituser(netloc) | 96 | auth, barehost = splituser(netloc) |
2360 | 48 | if auth is not None: | 97 | if auth is not None: |
2364 | 49 | source = urlparse.urlunparse((proto, barehost, path, params, query, fragment)) | 98 | source = urlunparse((proto, barehost, path, params, query, fragment)) |
2365 | 50 | username, password = urllib2.splitpasswd(auth) | 99 | username, password = splitpasswd(auth) |
2366 | 51 | passman = urllib2.HTTPPasswordMgrWithDefaultRealm() | 100 | passman = HTTPPasswordMgrWithDefaultRealm() |
2367 | 52 | # Realm is set to None in add_password to force the username and password | 101 | # Realm is set to None in add_password to force the username and password |
2368 | 53 | # to be used whatever the realm | 102 | # to be used whatever the realm |
2369 | 54 | passman.add_password(None, source, username, password) | 103 | passman.add_password(None, source, username, password) |
2374 | 55 | authhandler = urllib2.HTTPBasicAuthHandler(passman) | 104 | authhandler = HTTPBasicAuthHandler(passman) |
2375 | 56 | opener = urllib2.build_opener(authhandler) | 105 | opener = build_opener(authhandler) |
2376 | 57 | urllib2.install_opener(opener) | 106 | install_opener(opener) |
2377 | 58 | response = urllib2.urlopen(source) | 107 | response = urlopen(source) |
2378 | 59 | try: | 108 | try: |
2379 | 60 | with open(dest, 'w') as dest_file: | 109 | with open(dest, 'w') as dest_file: |
2380 | 61 | dest_file.write(response.read()) | 110 | dest_file.write(response.read()) |
2381 | @@ -91,17 +140,21 @@ | |||
2382 | 91 | url_parts = self.parse_url(source) | 140 | url_parts = self.parse_url(source) |
2383 | 92 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched') | 141 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched') |
2384 | 93 | if not os.path.exists(dest_dir): | 142 | if not os.path.exists(dest_dir): |
2386 | 94 | mkdir(dest_dir, perms=0755) | 143 | mkdir(dest_dir, perms=0o755) |
2387 | 95 | dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path)) | 144 | dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path)) |
2388 | 96 | try: | 145 | try: |
2389 | 97 | self.download(source, dld_file) | 146 | self.download(source, dld_file) |
2391 | 98 | except urllib2.URLError as e: | 147 | except URLError as e: |
2392 | 99 | raise UnhandledSource(e.reason) | 148 | raise UnhandledSource(e.reason) |
2393 | 100 | except OSError as e: | 149 | except OSError as e: |
2394 | 101 | raise UnhandledSource(e.strerror) | 150 | raise UnhandledSource(e.strerror) |
2396 | 102 | options = urlparse.parse_qs(url_parts.fragment) | 151 | options = parse_qs(url_parts.fragment) |
2397 | 103 | for key, value in options.items(): | 152 | for key, value in options.items(): |
2399 | 104 | if key in hashlib.algorithms: | 153 | if not six.PY3: |
2400 | 154 | algorithms = hashlib.algorithms | ||
2401 | 155 | else: | ||
2402 | 156 | algorithms = hashlib.algorithms_available | ||
2403 | 157 | if key in algorithms: | ||
2404 | 105 | check_hash(dld_file, value, key) | 158 | check_hash(dld_file, value, key) |
2405 | 106 | if checksum: | 159 | if checksum: |
2406 | 107 | check_hash(dld_file, checksum, hash_type) | 160 | check_hash(dld_file, checksum, hash_type) |
2407 | 108 | 161 | ||
2408 | === modified file 'hooks/charmhelpers/fetch/bzrurl.py' | |||
2409 | --- hooks/charmhelpers/fetch/bzrurl.py 2014-09-26 08:54:54 +0000 | |||
2410 | +++ hooks/charmhelpers/fetch/bzrurl.py 2015-03-31 04:43:29 +0000 | |||
2411 | @@ -1,3 +1,19 @@ | |||
2412 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2413 | 2 | # | ||
2414 | 3 | # This file is part of charm-helpers. | ||
2415 | 4 | # | ||
2416 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2417 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2418 | 7 | # published by the Free Software Foundation. | ||
2419 | 8 | # | ||
2420 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2421 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2422 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2423 | 12 | # GNU Lesser General Public License for more details. | ||
2424 | 13 | # | ||
2425 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2426 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2427 | 16 | |||
2428 | 1 | import os | 17 | import os |
2429 | 2 | from charmhelpers.fetch import ( | 18 | from charmhelpers.fetch import ( |
2430 | 3 | BaseFetchHandler, | 19 | BaseFetchHandler, |
2431 | @@ -5,12 +21,18 @@ | |||
2432 | 5 | ) | 21 | ) |
2433 | 6 | from charmhelpers.core.host import mkdir | 22 | from charmhelpers.core.host import mkdir |
2434 | 7 | 23 | ||
2435 | 24 | import six | ||
2436 | 25 | if six.PY3: | ||
2437 | 26 | raise ImportError('bzrlib does not support Python3') | ||
2438 | 27 | |||
2439 | 8 | try: | 28 | try: |
2440 | 9 | from bzrlib.branch import Branch | 29 | from bzrlib.branch import Branch |
2441 | 30 | from bzrlib import bzrdir, workingtree, errors | ||
2442 | 10 | except ImportError: | 31 | except ImportError: |
2443 | 11 | from charmhelpers.fetch import apt_install | 32 | from charmhelpers.fetch import apt_install |
2444 | 12 | apt_install("python-bzrlib") | 33 | apt_install("python-bzrlib") |
2445 | 13 | from bzrlib.branch import Branch | 34 | from bzrlib.branch import Branch |
2446 | 35 | from bzrlib import bzrdir, workingtree, errors | ||
2447 | 14 | 36 | ||
2448 | 15 | 37 | ||
2449 | 16 | class BzrUrlFetchHandler(BaseFetchHandler): | 38 | class BzrUrlFetchHandler(BaseFetchHandler): |
2450 | @@ -31,8 +53,14 @@ | |||
2451 | 31 | from bzrlib.plugin import load_plugins | 53 | from bzrlib.plugin import load_plugins |
2452 | 32 | load_plugins() | 54 | load_plugins() |
2453 | 33 | try: | 55 | try: |
2454 | 56 | local_branch = bzrdir.BzrDir.create_branch_convenience(dest) | ||
2455 | 57 | except errors.AlreadyControlDirError: | ||
2456 | 58 | local_branch = Branch.open(dest) | ||
2457 | 59 | try: | ||
2458 | 34 | remote_branch = Branch.open(source) | 60 | remote_branch = Branch.open(source) |
2460 | 35 | remote_branch.bzrdir.sprout(dest).open_branch() | 61 | remote_branch.push(local_branch) |
2461 | 62 | tree = workingtree.WorkingTree.open(dest) | ||
2462 | 63 | tree.update() | ||
2463 | 36 | except Exception as e: | 64 | except Exception as e: |
2464 | 37 | raise e | 65 | raise e |
2465 | 38 | 66 | ||
2466 | @@ -42,7 +70,7 @@ | |||
2467 | 42 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", | 70 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
2468 | 43 | branch_name) | 71 | branch_name) |
2469 | 44 | if not os.path.exists(dest_dir): | 72 | if not os.path.exists(dest_dir): |
2471 | 45 | mkdir(dest_dir, perms=0755) | 73 | mkdir(dest_dir, perms=0o755) |
2472 | 46 | try: | 74 | try: |
2473 | 47 | self.branch(source, dest_dir) | 75 | self.branch(source, dest_dir) |
2474 | 48 | except OSError as e: | 76 | except OSError as e: |
2475 | 49 | 77 | ||
2476 | === added file 'hooks/charmhelpers/fetch/giturl.py' | |||
2477 | --- hooks/charmhelpers/fetch/giturl.py 1970-01-01 00:00:00 +0000 | |||
2478 | +++ hooks/charmhelpers/fetch/giturl.py 2015-03-31 04:43:29 +0000 | |||
2479 | @@ -0,0 +1,71 @@ | |||
2480 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
2481 | 2 | # | ||
2482 | 3 | # This file is part of charm-helpers. | ||
2483 | 4 | # | ||
2484 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | ||
2485 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | ||
2486 | 7 | # published by the Free Software Foundation. | ||
2487 | 8 | # | ||
2488 | 9 | # charm-helpers is distributed in the hope that it will be useful, | ||
2489 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2490 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2491 | 12 | # GNU Lesser General Public License for more details. | ||
2492 | 13 | # | ||
2493 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2494 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2495 | 16 | |||
2496 | 17 | import os | ||
2497 | 18 | from charmhelpers.fetch import ( | ||
2498 | 19 | BaseFetchHandler, | ||
2499 | 20 | UnhandledSource | ||
2500 | 21 | ) | ||
2501 | 22 | from charmhelpers.core.host import mkdir | ||
2502 | 23 | |||
2503 | 24 | import six | ||
2504 | 25 | if six.PY3: | ||
2505 | 26 | raise ImportError('GitPython does not support Python 3') | ||
2506 | 27 | |||
2507 | 28 | try: | ||
2508 | 29 | from git import Repo | ||
2509 | 30 | except ImportError: | ||
2510 | 31 | from charmhelpers.fetch import apt_install | ||
2511 | 32 | apt_install("python-git") | ||
2512 | 33 | from git import Repo | ||
2513 | 34 | |||
2514 | 35 | from git.exc import GitCommandError # noqa E402 | ||
2515 | 36 | |||
2516 | 37 | |||
2517 | 38 | class GitUrlFetchHandler(BaseFetchHandler): | ||
2518 | 39 | """Handler for git branches via generic and github URLs""" | ||
2519 | 40 | def can_handle(self, source): | ||
2520 | 41 | url_parts = self.parse_url(source) | ||
2521 | 42 | # TODO (mattyw) no support for ssh git@ yet | ||
2522 | 43 | if url_parts.scheme not in ('http', 'https', 'git'): | ||
2523 | 44 | return False | ||
2524 | 45 | else: | ||
2525 | 46 | return True | ||
2526 | 47 | |||
2527 | 48 | def clone(self, source, dest, branch): | ||
2528 | 49 | if not self.can_handle(source): | ||
2529 | 50 | raise UnhandledSource("Cannot handle {}".format(source)) | ||
2530 | 51 | |||
2531 | 52 | repo = Repo.clone_from(source, dest) | ||
2532 | 53 | repo.git.checkout(branch) | ||
2533 | 54 | |||
2534 | 55 | def install(self, source, branch="master", dest=None): | ||
2535 | 56 | url_parts = self.parse_url(source) | ||
2536 | 57 | branch_name = url_parts.path.strip("/").split("/")[-1] | ||
2537 | 58 | if dest: | ||
2538 | 59 | dest_dir = os.path.join(dest, branch_name) | ||
2539 | 60 | else: | ||
2540 | 61 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", | ||
2541 | 62 | branch_name) | ||
2542 | 63 | if not os.path.exists(dest_dir): | ||
2543 | 64 | mkdir(dest_dir, perms=0o755) | ||
2544 | 65 | try: | ||
2545 | 66 | self.clone(source, dest_dir, branch) | ||
2546 | 67 | except GitCommandError as e: | ||
2547 | 68 | raise UnhandledSource(e.message) | ||
2548 | 69 | except OSError as e: | ||
2549 | 70 | raise UnhandledSource(e.strerror) | ||
2550 | 71 | return dest_dir | ||
2551 | 0 | 72 | ||
2552 | === modified file 'hooks/hooks.py' | |||
2553 | --- hooks/hooks.py 2014-09-25 07:00:05 +0000 | |||
2554 | +++ hooks/hooks.py 2015-03-31 04:43:29 +0000 | |||
2555 | @@ -7,9 +7,12 @@ | |||
2556 | 7 | 7 | ||
2557 | 8 | from charmhelpers.core.hookenv import ( | 8 | from charmhelpers.core.hookenv import ( |
2558 | 9 | Hooks, UnregisteredHookError, log) | 9 | Hooks, UnregisteredHookError, log) |
2559 | 10 | from charmhelpers.fetch import apt_install | ||
2560 | 10 | from shutil import rmtree | 11 | from shutil import rmtree |
2561 | 11 | from subprocess import CalledProcessError | 12 | from subprocess import CalledProcessError |
2562 | 12 | 13 | ||
2563 | 14 | from charmhelpers.contrib.charmsupport import nrpe | ||
2564 | 15 | |||
2565 | 13 | from common import ( | 16 | from common import ( |
2566 | 14 | write_json_file, JujuBroker, LandscapeBroker, chown) | 17 | write_json_file, JujuBroker, LandscapeBroker, chown) |
2567 | 15 | from ceph import ( | 18 | from ceph import ( |
2568 | @@ -41,6 +44,7 @@ | |||
2569 | 41 | match the data in the config | 44 | match the data in the config |
2570 | 42 | """ | 45 | """ |
2571 | 43 | juju_broker.log("In config-changed for %s" % landscape_broker.local_unit) | 46 | juju_broker.log("In config-changed for %s" % landscape_broker.local_unit) |
2572 | 47 | update_nrpe_config() | ||
2573 | 44 | if relation_data is None: | 48 | if relation_data is None: |
2574 | 45 | relation_data = {} | 49 | relation_data = {} |
2575 | 46 | service_config = juju_broker.get_service_config() | 50 | service_config = juju_broker.get_service_config() |
2576 | @@ -60,6 +64,7 @@ | |||
2577 | 60 | def upgrade_charm(landscape_broker=LANDSCAPE_BROKER): | 64 | def upgrade_charm(landscape_broker=LANDSCAPE_BROKER): |
2578 | 61 | """Idempotently upgrades state from older charms.""" | 65 | """Idempotently upgrades state from older charms.""" |
2579 | 62 | landscape_broker.config.reload() | 66 | landscape_broker.config.reload() |
2580 | 67 | update_nrpe_config() | ||
2581 | 63 | return migrate_old_juju_info(landscape_broker.config) | 68 | return migrate_old_juju_info(landscape_broker.config) |
2582 | 64 | 69 | ||
2583 | 65 | 70 | ||
2584 | @@ -147,6 +152,24 @@ | |||
2585 | 147 | landscape_broker.clear_registration() | 152 | landscape_broker.clear_registration() |
2586 | 148 | 153 | ||
2587 | 149 | 154 | ||
2588 | 155 | @hooks.hook('nrpe-external-master-relation-joined', | ||
2589 | 156 | 'nrpe-external-master-relation-changed') | ||
2590 | 157 | def update_nrpe_config(): | ||
2591 | 158 | # python-dbus is used by check_upstart_job | ||
2592 | 159 | apt_install('python-dbus') | ||
2593 | 160 | hostname = nrpe.get_nagios_hostname() | ||
2594 | 161 | current_unit = nrpe.get_nagios_unit_name() | ||
2595 | 162 | nrpe_setup = nrpe.NRPE(hostname=hostname) | ||
2596 | 163 | nrpe.add_init_service_checks(nrpe_setup, ['landscape-client'], current_unit) | ||
2597 | 164 | # process checks | ||
2598 | 165 | nrpe_setup.add_check( | ||
2599 | 166 | shortname='landscape-client_proc', | ||
2600 | 167 | description='Check Landscape Client process {%s}' % current_unit, | ||
2601 | 168 | check_cmd='check_procs -c 1: -a /usr/bin/landscape-client' | ||
2602 | 169 | ) | ||
2603 | 170 | nrpe_setup.write() | ||
2604 | 171 | |||
2605 | 172 | |||
2606 | 150 | def migrate_old_juju_info(client_config): | 173 | def migrate_old_juju_info(client_config): |
2607 | 151 | """ | 174 | """ |
2608 | 152 | Migrates data from the old meta-data.d directory into the new | 175 | Migrates data from the old meta-data.d directory into the new |
2609 | 153 | 176 | ||
2610 | === added symlink 'hooks/nrpe-external-master-relation-changed' | |||
2611 | === target is u'hooks.py' | |||
2612 | === added symlink 'hooks/nrpe-external-master-relation-joined' | |||
2613 | === target is u'hooks.py' | |||
2614 | === modified file 'metadata.yaml' | |||
2615 | --- metadata.yaml 2014-12-05 22:50:09 +0000 | |||
2616 | +++ metadata.yaml 2015-03-31 04:43:29 +0000 | |||
2617 | @@ -8,6 +8,10 @@ | |||
2618 | 8 | Landscape account. | 8 | Landscape account. |
2619 | 9 | subordinate: true | 9 | subordinate: true |
2620 | 10 | tags: [ ops, monitoring ] | 10 | tags: [ ops, monitoring ] |
2621 | 11 | provides: | ||
2622 | 12 | nrpe-external-master: | ||
2623 | 13 | interface: nrpe-external-master | ||
2624 | 14 | scope: container | ||
2625 | 11 | requires: | 15 | requires: |
2626 | 12 | container: | 16 | container: |
2627 | 13 | interface: juju-info | 17 | interface: juju-info |