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