Merge lp:~verterok/charms/trusty/logstash/merge-canonical-is-sa into lp:~tanuki/charms/trusty/logstash/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
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-relation-changed to use most recent elasticsearch unit and update charmhelpers)

Description of the change

Merge lp:~canonical-is-sa/charms/trusty/logstash/trunk:

  - add nagios checks for logstash and fixed client-relation-changed to use most recent elasticsearch unit.
  - update charmhelpers

To post a comment you must log in.
Revision history for this message
Celso Providelo (cprov) wrote :

+1, thank you.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2014-09-23 12:11:39 +0000
+++ charm-helpers.yaml 2015-09-08 18:07:26 +0000
@@ -5,3 +5,4 @@
5 - fetch5 - fetch
6 - payload6 - payload
7 - contrib.templating.jinja7 - contrib.templating.jinja
8 - contrib.charmsupport
89
=== modified file 'config.yaml'
--- config.yaml 2015-08-13 19:44:37 +0000
+++ config.yaml 2015-09-08 18:07:26 +0000
@@ -23,3 +23,27 @@
23 type: string23 type: string
24 default: ''24 default: ''
25 description: "Base64-encoded custom configuration content."25 description: "Base64-encoded custom configuration content."
26 nagios_context:
27 default: "juju"
28 type: string
29 description: |
30 Used by the nrpe subordinate charms.
31 A string that will be prepended to instance name to set the host name
32 in nagios. So for instance the hostname would be something like:
33 juju-myservice-0
34 If you're running multiple environments with the same services in them
35 this allows you to differentiate between them.
36 nagios_servicegroups:
37 default: ""
38 type: string
39 description: |
40 A comma-separated list of nagios servicegroups.
41 If left empty, the nagios_context will be used as the servicegroup
42 nagios_check_procs_params:
43 default: "-a /opt/logstash/lib/logstash/runner.rb -c 1:1"
44 type: string
45 description: The parameters to pass to the nrpe plugin check_procs.
46 nagios_check_tcp_params:
47 default: "--ssl -H localhost -p 5043 -c 0.3"
48 type: string
49 description: The parameters to pass to the nrpe plugin check_tcp.
2650
=== modified file 'hooks/client-relation-changed'
--- hooks/client-relation-changed 2015-09-08 16:23:29 +0000
+++ hooks/client-relation-changed 2015-09-08 18:07:26 +0000
@@ -30,7 +30,9 @@
30 with open('host_cache', 'r') as f:30 with open('host_cache', 'r') as f:
31 hosts = f.readlines()31 hosts = f.readlines()
3232
33 opts = {'hosts': hosts[0].rstrip()}33 # Use last host in list as it will be the most recently added
34 # and first host in list may not exist anymore! TODO fix that.
35 opts = {'hosts': hosts[-1].rstrip()}
3436
35 out = os.path.join(BASEPATH, 'conf.d', 'output-elasticsearch.conf')37 out = os.path.join(BASEPATH, 'conf.d', 'output-elasticsearch.conf')
36 with open(out, 'w') as p:38 with open(out, 'w') as p:
3739
=== modified file 'hooks/config-changed'
--- hooks/config-changed 2015-09-08 16:21:40 +0000
+++ hooks/config-changed 2015-09-08 18:07:26 +0000
@@ -49,6 +49,8 @@
49 # Restart the service when configuration has changed.49 # Restart the service when configuration has changed.
50 subprocess.check_output(shlex.split('hooks/start'))50 subprocess.check_output(shlex.split('hooks/start'))
5151
52 # TODO: should call update_nrpe_checks() here. See charmsupport/nrpe.py
53
5254
53def copy_config():55def copy_config():
54 files = os.listdir('templates')56 files = os.listdir('templates')
5557
=== modified file 'hooks/lumberjack-relation-changed' (properties changed: -x to +x)
=== added file 'hooks/nrpe-external-master-relation-changed'
--- hooks/nrpe-external-master-relation-changed 1970-01-01 00:00:00 +0000
+++ hooks/nrpe-external-master-relation-changed 2015-09-08 18:07:26 +0000
@@ -0,0 +1,39 @@
1#!/usr/bin/python
2
3import os
4import sys
5
6sys.path.insert(0, os.path.join(os.environ['CHARM_DIR'], 'lib'))
7
8from charmhelpers.core import hookenv
9from charmhelpers.contrib.charmsupport import nrpe
10
11hooks = hookenv.Hooks()
12log = hookenv.log
13
14@hooks.hook('nrpe-external-master-relation-changed')
15def update_nrpe_checks():
16 nrpe_compat = nrpe.NRPE()
17 conf = nrpe_compat.config
18 check_procs_params = conf.get('nagios_check_procs_params')
19 if check_procs_params:
20 nrpe_compat.add_check(
21 shortname='logstash_process',
22 description='Check logstash java process running',
23 check_cmd='check_procs %s' % check_procs_params
24 )
25 check_tcp_params = conf.get('nagios_check_tcp_params')
26 config_data = hookenv.config()
27 # Only setup lumberjack protocol if ssl cert and key are configured
28 if config_data['ssl_cert'] and config_data['ssl_key']:
29 if check_tcp_params:
30 nrpe_compat.add_check(
31 shortname='lumberjack_tcp',
32 description='Check logstash lumberjack input tcp port',
33 check_cmd='check_tcp %s' % check_tcp_params
34 )
35 nrpe_compat.write()
36
37if __name__ == "__main__":
38 # execute a hook based on the name the program is called by
39 hooks.execute(sys.argv)
040
=== added directory 'lib/charmhelpers/contrib/charmsupport'
=== added file 'lib/charmhelpers/contrib/charmsupport/__init__.py'
--- lib/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/charmsupport/__init__.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'lib/charmhelpers/contrib/charmsupport/nrpe.py'
--- lib/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/charmsupport/nrpe.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,360 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""Compatibility with the nrpe-external-master charm"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Matthew Wedgwood <matthew.wedgwood@canonical.com>
22
23import subprocess
24import pwd
25import grp
26import os
27import glob
28import shutil
29import re
30import shlex
31import yaml
32
33from charmhelpers.core.hookenv import (
34 config,
35 local_unit,
36 log,
37 relation_ids,
38 relation_set,
39 relations_of_type,
40)
41
42from charmhelpers.core.host import service
43
44# This module adds compatibility with the nrpe-external-master and plain nrpe
45# subordinate charms. To use it in your charm:
46#
47# 1. Update metadata.yaml
48#
49# provides:
50# (...)
51# nrpe-external-master:
52# interface: nrpe-external-master
53# scope: container
54#
55# and/or
56#
57# provides:
58# (...)
59# local-monitors:
60# interface: local-monitors
61# scope: container
62
63#
64# 2. Add the following to config.yaml
65#
66# nagios_context:
67# default: "juju"
68# type: string
69# description: |
70# Used by the nrpe subordinate charms.
71# A string that will be prepended to instance name to set the host name
72# in nagios. So for instance the hostname would be something like:
73# juju-myservice-0
74# If you're running multiple environments with the same services in them
75# this allows you to differentiate between them.
76# nagios_servicegroups:
77# default: ""
78# type: string
79# description: |
80# A comma-separated list of nagios servicegroups.
81# If left empty, the nagios_context will be used as the servicegroup
82#
83# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
84#
85# 4. Update your hooks.py with something like this:
86#
87# from charmsupport.nrpe import NRPE
88# (...)
89# def update_nrpe_config():
90# nrpe_compat = NRPE()
91# nrpe_compat.add_check(
92# shortname = "myservice",
93# description = "Check MyService",
94# check_cmd = "check_http -w 2 -c 10 http://localhost"
95# )
96# nrpe_compat.add_check(
97# "myservice_other",
98# "Check for widget failures",
99# check_cmd = "/srv/myapp/scripts/widget_check"
100# )
101# nrpe_compat.write()
102#
103# def config_changed():
104# (...)
105# update_nrpe_config()
106#
107# def nrpe_external_master_relation_changed():
108# update_nrpe_config()
109#
110# def local_monitors_relation_changed():
111# update_nrpe_config()
112#
113# 5. ln -s hooks.py nrpe-external-master-relation-changed
114# ln -s hooks.py local-monitors-relation-changed
115
116
117class CheckException(Exception):
118 pass
119
120
121class Check(object):
122 shortname_re = '[A-Za-z0-9-_]+$'
123 service_template = ("""
124#---------------------------------------------------
125# This file is Juju managed
126#---------------------------------------------------
127define service {{
128 use active-service
129 host_name {nagios_hostname}
130 service_description {nagios_hostname}[{shortname}] """
131 """{description}
132 check_command check_nrpe!{command}
133 servicegroups {nagios_servicegroup}
134}}
135""")
136
137 def __init__(self, shortname, description, check_cmd):
138 super(Check, self).__init__()
139 # XXX: could be better to calculate this from the service name
140 if not re.match(self.shortname_re, shortname):
141 raise CheckException("shortname must match {}".format(
142 Check.shortname_re))
143 self.shortname = shortname
144 self.command = "check_{}".format(shortname)
145 # Note: a set of invalid characters is defined by the
146 # Nagios server config
147 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)
150
151 def _locate_cmd(self, check_cmd):
152 search_path = (
153 '/usr/lib/nagios/plugins',
154 '/usr/local/lib/nagios/plugins',
155 )
156 parts = shlex.split(check_cmd)
157 for path in search_path:
158 if os.path.exists(os.path.join(path, parts[0])):
159 command = os.path.join(path, parts[0])
160 if len(parts) > 1:
161 command += " " + " ".join(parts[1:])
162 return command
163 log('Check command not found: {}'.format(parts[0]))
164 return ''
165
166 def write(self, nagios_context, hostname, nagios_servicegroups):
167 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
168 self.command)
169 with open(nrpe_check_file, 'w') as nrpe_check_config:
170 nrpe_check_config.write("# check {}\n".format(self.shortname))
171 nrpe_check_config.write("command[{}]={}\n".format(
172 self.command, self.check_cmd))
173
174 if not os.path.exists(NRPE.nagios_exportdir):
175 log('Not writing service config as {} is not accessible'.format(
176 NRPE.nagios_exportdir))
177 else:
178 self.write_service_config(nagios_context, hostname,
179 nagios_servicegroups)
180
181 def write_service_config(self, nagios_context, hostname,
182 nagios_servicegroups):
183 for f in os.listdir(NRPE.nagios_exportdir):
184 if re.search('.*{}.cfg'.format(self.command), f):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186
187 templ_vars = {
188 'nagios_hostname': hostname,
189 'nagios_servicegroup': nagios_servicegroups,
190 'description': self.description,
191 'shortname': self.shortname,
192 'command': self.command,
193 }
194 nrpe_service_text = Check.service_template.format(**templ_vars)
195 nrpe_service_file = '{}/service__{}_{}.cfg'.format(
196 NRPE.nagios_exportdir, hostname, self.command)
197 with open(nrpe_service_file, 'w') as nrpe_service_config:
198 nrpe_service_config.write(str(nrpe_service_text))
199
200 def run(self):
201 subprocess.call(self.check_cmd)
202
203
204class NRPE(object):
205 nagios_logdir = '/var/log/nagios'
206 nagios_exportdir = '/var/lib/nagios/export'
207 nrpe_confdir = '/etc/nagios/nrpe.d'
208
209 def __init__(self, hostname=None):
210 super(NRPE, self).__init__()
211 self.config = config()
212 self.nagios_context = self.config['nagios_context']
213 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
214 self.nagios_servicegroups = self.config['nagios_servicegroups']
215 else:
216 self.nagios_servicegroups = self.nagios_context
217 self.unit_name = local_unit().replace('/', '-')
218 if hostname:
219 self.hostname = hostname
220 else:
221 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
222 self.checks = []
223
224 def add_check(self, *args, **kwargs):
225 self.checks.append(Check(*args, **kwargs))
226
227 def write(self):
228 try:
229 nagios_uid = pwd.getpwnam('nagios').pw_uid
230 nagios_gid = grp.getgrnam('nagios').gr_gid
231 except:
232 log("Nagios user not set up, nrpe checks not updated")
233 return
234
235 if not os.path.exists(NRPE.nagios_logdir):
236 os.mkdir(NRPE.nagios_logdir)
237 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
238
239 nrpe_monitors = {}
240 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
241 for nrpecheck in self.checks:
242 nrpecheck.write(self.nagios_context, self.hostname,
243 self.nagios_servicegroups)
244 nrpe_monitors[nrpecheck.shortname] = {
245 "command": nrpecheck.command,
246 }
247
248 service('restart', 'nagios-nrpe-server')
249
250 monitor_ids = relation_ids("local-monitors") + \
251 relation_ids("nrpe-external-master")
252 for rid in monitor_ids:
253 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
254
255
256def get_nagios_hostcontext(relation_name='nrpe-external-master'):
257 """
258 Query relation with nrpe subordinate, return the nagios_host_context
259
260 :param str relation_name: Name of relation nrpe sub joined to
261 """
262 for rel in relations_of_type(relation_name):
263 if 'nagios_hostname' in rel:
264 return rel['nagios_host_context']
265
266
267def get_nagios_hostname(relation_name='nrpe-external-master'):
268 """
269 Query relation with nrpe subordinate, return the nagios_hostname
270
271 :param str relation_name: Name of relation nrpe sub joined to
272 """
273 for rel in relations_of_type(relation_name):
274 if 'nagios_hostname' in rel:
275 return rel['nagios_hostname']
276
277
278def get_nagios_unit_name(relation_name='nrpe-external-master'):
279 """
280 Return the nagios unit name prepended with host_context if needed
281
282 :param str relation_name: Name of relation nrpe sub joined to
283 """
284 host_context = get_nagios_hostcontext(relation_name)
285 if host_context:
286 unit = "%s:%s" % (host_context, local_unit())
287 else:
288 unit = local_unit()
289 return unit
290
291
292def add_init_service_checks(nrpe, services, unit_name):
293 """
294 Add checks for each service in list
295
296 :param NRPE nrpe: NRPE object to add check to
297 :param list services: List of services to check
298 :param str unit_name: Unit name to use in check description
299 """
300 for svc in services:
301 upstart_init = '/etc/init/%s.conf' % svc
302 sysv_init = '/etc/init.d/%s' % svc
303 if os.path.exists(upstart_init):
304 nrpe.add_check(
305 shortname=svc,
306 description='process check {%s}' % unit_name,
307 check_cmd='check_upstart_job %s' % svc
308 )
309 elif os.path.exists(sysv_init):
310 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
311 cron_file = ('*/5 * * * * root '
312 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
313 '-s /etc/init.d/%s status > '
314 '/var/lib/nagios/service-check-%s.txt\n' % (svc,
315 svc)
316 )
317 f = open(cronpath, 'w')
318 f.write(cron_file)
319 f.close()
320 nrpe.add_check(
321 shortname=svc,
322 description='process check {%s}' % unit_name,
323 check_cmd='check_status_file.py -f '
324 '/var/lib/nagios/service-check-%s.txt' % svc,
325 )
326
327
328def copy_nrpe_checks():
329 """
330 Copy the nrpe checks into place
331
332 """
333 NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
334 nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
335 'charmhelpers', 'contrib', 'openstack',
336 'files')
337
338 if not os.path.exists(NAGIOS_PLUGINS):
339 os.makedirs(NAGIOS_PLUGINS)
340 for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
341 if os.path.isfile(fname):
342 shutil.copy2(fname,
343 os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
344
345
346def add_haproxy_checks(nrpe, unit_name):
347 """
348 Add checks for each service in list
349
350 :param NRPE nrpe: NRPE object to add check to
351 :param str unit_name: Unit name to use in check description
352 """
353 nrpe.add_check(
354 shortname='haproxy_servers',
355 description='Check HAProxy {%s}' % unit_name,
356 check_cmd='check_haproxy.sh')
357 nrpe.add_check(
358 shortname='haproxy_queue',
359 description='Check HAProxy queue depth {%s}' % unit_name,
360 check_cmd='check_haproxy_queue_depth.sh')
0361
=== added file 'lib/charmhelpers/contrib/charmsupport/volumes.py'
--- lib/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/contrib/charmsupport/volumes.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,175 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17'''
18Functions for managing volumes in juju units. One volume is supported per unit.
19Subordinates may have their own storage, provided it is on its own partition.
20
21Configuration stanzas::
22
23 volume-ephemeral:
24 type: boolean
25 default: true
26 description: >
27 If false, a volume is mounted as sepecified in "volume-map"
28 If true, ephemeral storage will be used, meaning that log data
29 will only exist as long as the machine. YOU HAVE BEEN WARNED.
30 volume-map:
31 type: string
32 default: {}
33 description: >
34 YAML map of units to device names, e.g:
35 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36 Service units will raise a configure-error if volume-ephemeral
37 is 'true' and no volume-map value is set. Use 'juju set' to set a
38 value and 'juju resolved' to complete configuration.
39
40Usage::
41
42 from charmsupport.volumes import configure_volume, VolumeConfigurationError
43 from charmsupport.hookenv import log, ERROR
44 def post_mount_hook():
45 stop_service('myservice')
46 def post_mount_hook():
47 start_service('myservice')
48
49 if __name__ == '__main__':
50 try:
51 configure_volume(before_change=pre_mount_hook,
52 after_change=post_mount_hook)
53 except VolumeConfigurationError:
54 log('Storage could not be configured', ERROR)
55
56'''
57
58# XXX: Known limitations
59# - fstab is neither consulted nor updated
60
61import os
62from charmhelpers.core import hookenv
63from charmhelpers.core import host
64import yaml
65
66
67MOUNT_BASE = '/srv/juju/volumes'
68
69
70class VolumeConfigurationError(Exception):
71 '''Volume configuration data is missing or invalid'''
72 pass
73
74
75def get_config():
76 '''Gather and sanity-check volume configuration data'''
77 volume_config = {}
78 config = hookenv.config()
79
80 errors = False
81
82 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83 volume_config['ephemeral'] = True
84 else:
85 volume_config['ephemeral'] = False
86
87 try:
88 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89 except yaml.YAMLError as e:
90 hookenv.log("Error parsing YAML volume-map: {}".format(e),
91 hookenv.ERROR)
92 errors = True
93 if volume_map is None:
94 # probably an empty string
95 volume_map = {}
96 elif not isinstance(volume_map, dict):
97 hookenv.log("Volume-map should be a dictionary, not {}".format(
98 type(volume_map)))
99 errors = True
100
101 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102 if volume_config['device'] and volume_config['ephemeral']:
103 # asked for ephemeral storage but also defined a volume ID
104 hookenv.log('A volume is defined for this unit, but ephemeral '
105 'storage was requested', hookenv.ERROR)
106 errors = True
107 elif not volume_config['device'] and not volume_config['ephemeral']:
108 # asked for permanent storage but did not define volume ID
109 hookenv.log('Ephemeral storage was requested, but there is no volume '
110 'defined for this unit.', hookenv.ERROR)
111 errors = True
112
113 unit_mount_name = hookenv.local_unit().replace('/', '-')
114 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
115
116 if errors:
117 return None
118 return volume_config
119
120
121def mount_volume(config):
122 if os.path.exists(config['mountpoint']):
123 if not os.path.isdir(config['mountpoint']):
124 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125 raise VolumeConfigurationError()
126 else:
127 host.mkdir(config['mountpoint'])
128 if os.path.ismount(config['mountpoint']):
129 unmount_volume(config)
130 if not host.mount(config['device'], config['mountpoint'], persist=True):
131 raise VolumeConfigurationError()
132
133
134def unmount_volume(config):
135 if os.path.ismount(config['mountpoint']):
136 if not host.umount(config['mountpoint'], persist=True):
137 raise VolumeConfigurationError()
138
139
140def managed_mounts():
141 '''List of all mounted managed volumes'''
142 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
143
144
145def configure_volume(before_change=lambda: None, after_change=lambda: None):
146 '''Set up storage (or don't) according to the charm's volume configuration.
147 Returns the mount point or "ephemeral". before_change and after_change
148 are optional functions to be called if the volume configuration changes.
149 '''
150
151 config = get_config()
152 if not config:
153 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154 raise VolumeConfigurationError()
155
156 if config['ephemeral']:
157 if os.path.ismount(config['mountpoint']):
158 before_change()
159 unmount_volume(config)
160 after_change()
161 return 'ephemeral'
162 else:
163 # persistent storage
164 if os.path.ismount(config['mountpoint']):
165 mounts = dict(managed_mounts())
166 if mounts.get(config['mountpoint']) != config['device']:
167 before_change()
168 unmount_volume(config)
169 mount_volume(config)
170 after_change()
171 else:
172 before_change()
173 mount_volume(config)
174 after_change()
175 return config['mountpoint']
0176
=== modified file 'lib/charmhelpers/contrib/templating/__init__.py'
--- lib/charmhelpers/contrib/templating/__init__.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/contrib/templating/__init__.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== modified file 'lib/charmhelpers/contrib/templating/jinja.py'
--- lib/charmhelpers/contrib/templating/jinja.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/contrib/templating/jinja.py 2015-09-08 18:07:26 +0000
@@ -1,21 +1,37 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1"""17"""
2Templating using the python-jinja2 package.18Templating using the python-jinja2 package.
3"""19"""
4from charmhelpers.fetch import (20import six
5 apt_install,21from charmhelpers.fetch import apt_install
6)
7
8
9DEFAULT_TEMPLATES_DIR = 'templates'
10
11
12try:22try:
13 import jinja223 import jinja2
14except ImportError:24except ImportError:
15 apt_install(["python-jinja2"])25 if six.PY3:
26 apt_install(["python3-jinja2"])
27 else:
28 apt_install(["python-jinja2"])
16 import jinja229 import jinja2
1730
1831
32DEFAULT_TEMPLATES_DIR = 'templates'
33
34
19def render(template_name, context, template_dir=DEFAULT_TEMPLATES_DIR):35def render(template_name, context, template_dir=DEFAULT_TEMPLATES_DIR):
20 templates = jinja2.Environment(36 templates = jinja2.Environment(
21 loader=jinja2.FileSystemLoader(template_dir))37 loader=jinja2.FileSystemLoader(template_dir))
2238
=== modified file 'lib/charmhelpers/core/__init__.py'
--- lib/charmhelpers/core/__init__.py 2014-07-17 16:38:17 +0000
+++ lib/charmhelpers/core/__init__.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'lib/charmhelpers/core/decorators.py'
--- lib/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/core/decorators.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,57 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2014 Canonical Ltd.
19#
20# Authors:
21# Edward Hope-Morley <opentastic@gmail.com>
22#
23
24import time
25
26from charmhelpers.core.hookenv import (
27 log,
28 INFO,
29)
30
31
32def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
33 """If the decorated function raises exception exc_type, allow num_retries
34 retry attempts before raise the exception.
35 """
36 def _retry_on_exception_inner_1(f):
37 def _retry_on_exception_inner_2(*args, **kwargs):
38 retries = num_retries
39 multiplier = 1
40 while True:
41 try:
42 return f(*args, **kwargs)
43 except exc_type:
44 if not retries:
45 raise
46
47 delay = base_delay * multiplier
48 multiplier += 1
49 log("Retrying '%s' %d more times (delay=%s)" %
50 (f.__name__, retries, delay), level=INFO)
51 retries -= 1
52 if delay:
53 time.sleep(delay)
54
55 return _retry_on_exception_inner_2
56
57 return _retry_on_exception_inner_1
058
=== modified file 'lib/charmhelpers/core/fstab.py'
--- lib/charmhelpers/core/fstab.py 2014-07-17 16:38:17 +0000
+++ lib/charmhelpers/core/fstab.py 2015-09-08 18:07:26 +0000
@@ -1,12 +1,29 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20import io
21import os
22
4__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'23__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
524
6import os25
726class Fstab(io.FileIO):
8
9class Fstab(file):
10 """This class extends file in order to implement a file reader/writer27 """This class extends file in order to implement a file reader/writer
11 for file `/etc/fstab`28 for file `/etc/fstab`
12 """29 """
@@ -24,8 +41,8 @@
24 options = "defaults"41 options = "defaults"
2542
26 self.options = options43 self.options = options
27 self.d = d44 self.d = int(d)
28 self.p = p45 self.p = int(p)
2946
30 def __eq__(self, o):47 def __eq__(self, o):
31 return str(self) == str(o)48 return str(self) == str(o)
@@ -45,7 +62,7 @@
45 self._path = path62 self._path = path
46 else:63 else:
47 self._path = self.DEFAULT_PATH64 self._path = self.DEFAULT_PATH
48 file.__init__(self, self._path, 'r+')65 super(Fstab, self).__init__(self._path, 'rb+')
4966
50 def _hydrate_entry(self, line):67 def _hydrate_entry(self, line):
51 # NOTE: use split with no arguments to split on any68 # NOTE: use split with no arguments to split on any
@@ -58,8 +75,9 @@
58 def entries(self):75 def entries(self):
59 self.seek(0)76 self.seek(0)
60 for line in self.readlines():77 for line in self.readlines():
78 line = line.decode('us-ascii')
61 try:79 try:
62 if not line.startswith("#"):80 if line.strip() and not line.strip().startswith("#"):
63 yield self._hydrate_entry(line)81 yield self._hydrate_entry(line)
64 except ValueError:82 except ValueError:
65 pass83 pass
@@ -75,18 +93,18 @@
75 if self.get_entry_by_attr('device', entry.device):93 if self.get_entry_by_attr('device', entry.device):
76 return False94 return False
7795
78 self.write(str(entry) + '\n')96 self.write((str(entry) + '\n').encode('us-ascii'))
79 self.truncate()97 self.truncate()
80 return entry98 return entry
8199
82 def remove_entry(self, entry):100 def remove_entry(self, entry):
83 self.seek(0)101 self.seek(0)
84102
85 lines = self.readlines()103 lines = [l.decode('us-ascii') for l in self.readlines()]
86104
87 found = False105 found = False
88 for index, line in enumerate(lines):106 for index, line in enumerate(lines):
89 if not line.startswith("#"):107 if line.strip() and not line.strip().startswith("#"):
90 if self._hydrate_entry(line) == entry:108 if self._hydrate_entry(line) == entry:
91 found = True109 found = True
92 break110 break
@@ -97,7 +115,7 @@
97 lines.remove(line)115 lines.remove(line)
98116
99 self.seek(0)117 self.seek(0)
100 self.write(''.join(lines))118 self.write(''.join(lines).encode('us-ascii'))
101 self.truncate()119 self.truncate()
102 return True120 return True
103121
104122
=== modified file 'lib/charmhelpers/core/hookenv.py'
--- lib/charmhelpers/core/hookenv.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/core/hookenv.py 2015-09-08 18:07:26 +0000
@@ -1,17 +1,40 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1"Interactions with the Juju environment"17"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.18# Copyright 2013 Canonical Ltd.
3#19#
4# Authors:20# Authors:
5# Charm Helpers Developers <juju@lists.ubuntu.com>21# Charm Helpers Developers <juju@lists.ubuntu.com>
622
23from __future__ import print_function
7import os24import os
8import json25import json
9import yaml26import yaml
10import subprocess27import subprocess
11import sys28import sys
12import UserDict29import errno
13from subprocess import CalledProcessError30from subprocess import CalledProcessError
1431
32import six
33if not six.PY3:
34 from UserDict import UserDict
35else:
36 from collections import UserDict
37
15CRITICAL = "CRITICAL"38CRITICAL = "CRITICAL"
16ERROR = "ERROR"39ERROR = "ERROR"
17WARNING = "WARNING"40WARNING = "WARNING"
@@ -63,16 +86,29 @@
63 command = ['juju-log']86 command = ['juju-log']
64 if level:87 if level:
65 command += ['-l', level]88 command += ['-l', level]
89 if not isinstance(message, six.string_types):
90 message = repr(message)
66 command += [message]91 command += [message]
67 subprocess.call(command)92 # Missing juju-log should not cause failures in unit tests
6893 # Send log output to stderr
6994 try:
70class Serializable(UserDict.IterableUserDict):95 subprocess.call(command)
96 except OSError as e:
97 if e.errno == errno.ENOENT:
98 if level:
99 message = "{}: {}".format(level, message)
100 message = "juju-log: {}".format(message)
101 print(message, file=sys.stderr)
102 else:
103 raise
104
105
106class Serializable(UserDict):
71 """Wrapper, an object that can be serialized to yaml or json"""107 """Wrapper, an object that can be serialized to yaml or json"""
72108
73 def __init__(self, obj):109 def __init__(self, obj):
74 # wrap the object110 # wrap the object
75 UserDict.IterableUserDict.__init__(self)111 UserDict.__init__(self)
76 self.data = obj112 self.data = obj
77113
78 def __getattr__(self, attr):114 def __getattr__(self, attr):
@@ -214,6 +250,12 @@
214 except KeyError:250 except KeyError:
215 return (self._prev_dict or {})[key]251 return (self._prev_dict or {})[key]
216252
253 def keys(self):
254 prev_keys = []
255 if self._prev_dict is not None:
256 prev_keys = self._prev_dict.keys()
257 return list(set(prev_keys + list(dict.keys(self))))
258
217 def load_previous(self, path=None):259 def load_previous(self, path=None):
218 """Load previous copy of config from disk.260 """Load previous copy of config from disk.
219261
@@ -263,7 +305,7 @@
263305
264 """306 """
265 if self._prev_dict:307 if self._prev_dict:
266 for k, v in self._prev_dict.iteritems():308 for k, v in six.iteritems(self._prev_dict):
267 if k not in self:309 if k not in self:
268 self[k] = v310 self[k] = v
269 with open(self.path, 'w') as f:311 with open(self.path, 'w') as f:
@@ -278,7 +320,8 @@
278 config_cmd_line.append(scope)320 config_cmd_line.append(scope)
279 config_cmd_line.append('--format=json')321 config_cmd_line.append('--format=json')
280 try:322 try:
281 config_data = json.loads(subprocess.check_output(config_cmd_line))323 config_data = json.loads(
324 subprocess.check_output(config_cmd_line).decode('UTF-8'))
282 if scope is not None:325 if scope is not None:
283 return config_data326 return config_data
284 return Config(config_data)327 return Config(config_data)
@@ -297,10 +340,10 @@
297 if unit:340 if unit:
298 _args.append(unit)341 _args.append(unit)
299 try:342 try:
300 return json.loads(subprocess.check_output(_args))343 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
301 except ValueError:344 except ValueError:
302 return None345 return None
303 except CalledProcessError, e:346 except CalledProcessError as e:
304 if e.returncode == 2:347 if e.returncode == 2:
305 return None348 return None
306 raise349 raise
@@ -312,7 +355,7 @@
312 relation_cmd_line = ['relation-set']355 relation_cmd_line = ['relation-set']
313 if relation_id is not None:356 if relation_id is not None:
314 relation_cmd_line.extend(('-r', relation_id))357 relation_cmd_line.extend(('-r', relation_id))
315 for k, v in (relation_settings.items() + kwargs.items()):358 for k, v in (list(relation_settings.items()) + list(kwargs.items())):
316 if v is None:359 if v is None:
317 relation_cmd_line.append('{}='.format(k))360 relation_cmd_line.append('{}='.format(k))
318 else:361 else:
@@ -329,7 +372,8 @@
329 relid_cmd_line = ['relation-ids', '--format=json']372 relid_cmd_line = ['relation-ids', '--format=json']
330 if reltype is not None:373 if reltype is not None:
331 relid_cmd_line.append(reltype)374 relid_cmd_line.append(reltype)
332 return json.loads(subprocess.check_output(relid_cmd_line)) or []375 return json.loads(
376 subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
333 return []377 return []
334378
335379
@@ -340,7 +384,8 @@
340 units_cmd_line = ['relation-list', '--format=json']384 units_cmd_line = ['relation-list', '--format=json']
341 if relid is not None:385 if relid is not None:
342 units_cmd_line.extend(('-r', relid))386 units_cmd_line.extend(('-r', relid))
343 return json.loads(subprocess.check_output(units_cmd_line)) or []387 return json.loads(
388 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
344389
345390
346@cached391@cached
@@ -380,21 +425,31 @@
380425
381426
382@cached427@cached
428def metadata():
429 """Get the current charm metadata.yaml contents as a python object"""
430 with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
431 return yaml.safe_load(md)
432
433
434@cached
383def relation_types():435def relation_types():
384 """Get a list of relation types supported by this charm"""436 """Get a list of relation types supported by this charm"""
385 charmdir = os.environ.get('CHARM_DIR', '')
386 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
387 md = yaml.safe_load(mdf)
388 rel_types = []437 rel_types = []
438 md = metadata()
389 for key in ('provides', 'requires', 'peers'):439 for key in ('provides', 'requires', 'peers'):
390 section = md.get(key)440 section = md.get(key)
391 if section:441 if section:
392 rel_types.extend(section.keys())442 rel_types.extend(section.keys())
393 mdf.close()
394 return rel_types443 return rel_types
395444
396445
397@cached446@cached
447def charm_name():
448 """Get the name of the current charm as is specified on metadata.yaml"""
449 return metadata().get('name')
450
451
452@cached
398def relations():453def relations():
399 """Get a nested dictionary of relation data for all related units"""454 """Get a nested dictionary of relation data for all related units"""
400 rels = {}455 rels = {}
@@ -449,7 +504,7 @@
449 """Get the unit ID for the remote unit"""504 """Get the unit ID for the remote unit"""
450 _args = ['unit-get', '--format=json', attribute]505 _args = ['unit-get', '--format=json', attribute]
451 try:506 try:
452 return json.loads(subprocess.check_output(_args))507 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
453 except ValueError:508 except ValueError:
454 return None509 return None
455510
@@ -486,9 +541,10 @@
486 hooks.execute(sys.argv)541 hooks.execute(sys.argv)
487 """542 """
488543
489 def __init__(self):544 def __init__(self, config_save=True):
490 super(Hooks, self).__init__()545 super(Hooks, self).__init__()
491 self._hooks = {}546 self._hooks = {}
547 self._config_save = config_save
492548
493 def register(self, name, function):549 def register(self, name, function):
494 """Register a hook"""550 """Register a hook"""
@@ -499,9 +555,10 @@
499 hook_name = os.path.basename(args[0])555 hook_name = os.path.basename(args[0])
500 if hook_name in self._hooks:556 if hook_name in self._hooks:
501 self._hooks[hook_name]()557 self._hooks[hook_name]()
502 cfg = config()558 if self._config_save:
503 if cfg.implicit_save:559 cfg = config()
504 cfg.save()560 if cfg.implicit_save:
561 cfg.save()
505 else:562 else:
506 raise UnregisteredHookError(hook_name)563 raise UnregisteredHookError(hook_name)
507564
@@ -522,3 +579,29 @@
522def charm_dir():579def charm_dir():
523 """Return the root directory of the current charm"""580 """Return the root directory of the current charm"""
524 return os.environ.get('CHARM_DIR')581 return os.environ.get('CHARM_DIR')
582
583
584@cached
585def action_get(key=None):
586 """Gets the value of an action parameter, or all key/value param pairs"""
587 cmd = ['action-get']
588 if key is not None:
589 cmd.append(key)
590 cmd.append('--format=json')
591 action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
592 return action_data
593
594
595def action_set(values):
596 """Sets the values to be returned after the action finishes"""
597 cmd = ['action-set']
598 for k, v in list(values.items()):
599 cmd.append('{}={}'.format(k, v))
600 subprocess.check_call(cmd)
601
602
603def action_fail(message):
604 """Sets the action status to failed and sets the error message.
605
606 The results set by action_set are preserved."""
607 subprocess.check_call(['action-fail', message])
525608
=== modified file 'lib/charmhelpers/core/host.py'
--- lib/charmhelpers/core/host.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/core/host.py 2015-09-08 18:07:26 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1"""Tools for working with the host system"""17"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.18# Copyright 2012 Canonical Ltd.
3#19#
@@ -6,19 +22,20 @@
6# Matthew Wedgwood <matthew.wedgwood@canonical.com>22# Matthew Wedgwood <matthew.wedgwood@canonical.com>
723
8import os24import os
25import re
9import pwd26import pwd
10import grp27import grp
11import random28import random
12import string29import string
13import subprocess30import subprocess
14import hashlib31import hashlib
15import shutil
16from contextlib import contextmanager32from contextlib import contextmanager
17
18from collections import OrderedDict33from collections import OrderedDict
1934
20from hookenv import log35import six
21from fstab import Fstab36
37from .hookenv import log
38from .fstab import Fstab
2239
2340
24def service_start(service_name):41def service_start(service_name):
@@ -54,7 +71,9 @@
54def service_running(service):71def service_running(service):
55 """Determine whether a system service is running"""72 """Determine whether a system service is running"""
56 try:73 try:
57 output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)74 output = subprocess.check_output(
75 ['service', service, 'status'],
76 stderr=subprocess.STDOUT).decode('UTF-8')
58 except subprocess.CalledProcessError:77 except subprocess.CalledProcessError:
59 return False78 return False
60 else:79 else:
@@ -67,9 +86,11 @@
67def service_available(service_name):86def service_available(service_name):
68 """Determine whether a system service is available"""87 """Determine whether a system service is available"""
69 try:88 try:
70 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)89 subprocess.check_output(
71 except subprocess.CalledProcessError:90 ['service', service_name, 'status'],
72 return False91 stderr=subprocess.STDOUT).decode('UTF-8')
92 except subprocess.CalledProcessError as e:
93 return 'unrecognized service' not in e.output
73 else:94 else:
74 return True95 return True
7596
@@ -96,6 +117,26 @@
96 return user_info117 return user_info
97118
98119
120def add_group(group_name, system_group=False):
121 """Add a group to the system"""
122 try:
123 group_info = grp.getgrnam(group_name)
124 log('group {0} already exists!'.format(group_name))
125 except KeyError:
126 log('creating group {0}'.format(group_name))
127 cmd = ['addgroup']
128 if system_group:
129 cmd.append('--system')
130 else:
131 cmd.extend([
132 '--group',
133 ])
134 cmd.append(group_name)
135 subprocess.check_call(cmd)
136 group_info = grp.getgrnam(group_name)
137 return group_info
138
139
99def add_user_to_group(username, group):140def add_user_to_group(username, group):
100 """Add a user to a group"""141 """Add a user to a group"""
101 cmd = [142 cmd = [
@@ -115,7 +156,7 @@
115 cmd.append(from_path)156 cmd.append(from_path)
116 cmd.append(to_path)157 cmd.append(to_path)
117 log(" ".join(cmd))158 log(" ".join(cmd))
118 return subprocess.check_output(cmd).strip()159 return subprocess.check_output(cmd).decode('UTF-8').strip()
119160
120161
121def symlink(source, destination):162def symlink(source, destination):
@@ -130,28 +171,31 @@
130 subprocess.check_call(cmd)171 subprocess.check_call(cmd)
131172
132173
133def mkdir(path, owner='root', group='root', perms=0555, force=False):174def mkdir(path, owner='root', group='root', perms=0o555, force=False):
134 """Create a directory"""175 """Create a directory"""
135 log("Making dir {} {}:{} {:o}".format(path, owner, group,176 log("Making dir {} {}:{} {:o}".format(path, owner, group,
136 perms))177 perms))
137 uid = pwd.getpwnam(owner).pw_uid178 uid = pwd.getpwnam(owner).pw_uid
138 gid = grp.getgrnam(group).gr_gid179 gid = grp.getgrnam(group).gr_gid
139 realpath = os.path.abspath(path)180 realpath = os.path.abspath(path)
140 if os.path.exists(realpath):181 path_exists = os.path.exists(realpath)
141 if force and not os.path.isdir(realpath):182 if path_exists and force:
183 if not os.path.isdir(realpath):
142 log("Removing non-directory file {} prior to mkdir()".format(path))184 log("Removing non-directory file {} prior to mkdir()".format(path))
143 os.unlink(realpath)185 os.unlink(realpath)
144 else:186 os.makedirs(realpath, perms)
187 elif not path_exists:
145 os.makedirs(realpath, perms)188 os.makedirs(realpath, perms)
146 os.chown(realpath, uid, gid)189 os.chown(realpath, uid, gid)
147190 os.chmod(realpath, perms)
148191
149def write_file(path, content, owner='root', group='root', perms=0444):192
150 """Create or overwrite a file with the contents of a string"""193def write_file(path, content, owner='root', group='root', perms=0o444):
194 """Create or overwrite a file with the contents of a byte string."""
151 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))195 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
152 uid = pwd.getpwnam(owner).pw_uid196 uid = pwd.getpwnam(owner).pw_uid
153 gid = grp.getgrnam(group).gr_gid197 gid = grp.getgrnam(group).gr_gid
154 with open(path, 'w') as target:198 with open(path, 'wb') as target:
155 os.fchown(target.fileno(), uid, gid)199 os.fchown(target.fileno(), uid, gid)
156 os.fchmod(target.fileno(), perms)200 os.fchmod(target.fileno(), perms)
157 target.write(content)201 target.write(content)
@@ -177,7 +221,7 @@
177 cmd_args.extend([device, mountpoint])221 cmd_args.extend([device, mountpoint])
178 try:222 try:
179 subprocess.check_output(cmd_args)223 subprocess.check_output(cmd_args)
180 except subprocess.CalledProcessError, e:224 except subprocess.CalledProcessError as e:
181 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))225 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
182 return False226 return False
183227
@@ -191,7 +235,7 @@
191 cmd_args = ['umount', mountpoint]235 cmd_args = ['umount', mountpoint]
192 try:236 try:
193 subprocess.check_output(cmd_args)237 subprocess.check_output(cmd_args)
194 except subprocess.CalledProcessError, e:238 except subprocess.CalledProcessError as e:
195 log('Error unmounting {}\n{}'.format(mountpoint, e.output))239 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
196 return False240 return False
197241
@@ -218,8 +262,8 @@
218 """262 """
219 if os.path.exists(path):263 if os.path.exists(path):
220 h = getattr(hashlib, hash_type)()264 h = getattr(hashlib, hash_type)()
221 with open(path, 'r') as source:265 with open(path, 'rb') as source:
222 h.update(source.read()) # IGNORE:E1101 - it does have update266 h.update(source.read())
223 return h.hexdigest()267 return h.hexdigest()
224 else:268 else:
225 return None269 return None
@@ -229,12 +273,12 @@
229 """273 """
230 Validate a file using a cryptographic checksum.274 Validate a file using a cryptographic checksum.
231275
232
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.
234 :param str hash_type: Hash algorithm used to generate :param:`checksum`.277 :param str hash_type: Hash algorithm used to generate `checksum`.
235 Can be any hash alrgorithm supported by :mod:`hashlib`,278 Can be any hash alrgorithm supported by :mod:`hashlib`,
236 such as md5, sha1, sha256, sha512, etc.279 such as md5, sha1, sha256, sha512, etc.
237 :raises ChecksumError: If the file fails the checksum280 :raises ChecksumError: If the file fails the checksum
281
238 """282 """
239 actual_checksum = file_hash(path, hash_type)283 actual_checksum = file_hash(path, hash_type)
240 if checksum != actual_checksum:284 if checksum != actual_checksum:
@@ -261,11 +305,11 @@
261 ceph_client_changed function.305 ceph_client_changed function.
262 """306 """
263 def wrap(f):307 def wrap(f):
264 def wrapped_f(*args):308 def wrapped_f(*args, **kwargs):
265 checksums = {}309 checksums = {}
266 for path in restart_map:310 for path in restart_map:
267 checksums[path] = file_hash(path)311 checksums[path] = file_hash(path)
268 f(*args)312 f(*args, **kwargs)
269 restarts = []313 restarts = []
270 for path in restart_map:314 for path in restart_map:
271 if checksums[path] != file_hash(path):315 if checksums[path] != file_hash(path):
@@ -295,29 +339,39 @@
295def pwgen(length=None):339def pwgen(length=None):
296 """Generate a random pasword."""340 """Generate a random pasword."""
297 if length is None:341 if length is None:
342 # A random length is ok to use a weak PRNG
298 length = random.choice(range(35, 45))343 length = random.choice(range(35, 45))
299 alphanumeric_chars = [344 alphanumeric_chars = [
300 l for l in (string.letters + string.digits)345 l for l in (string.ascii_letters + string.digits)
301 if l not in 'l0QD1vAEIOUaeiou']346 if l not in 'l0QD1vAEIOUaeiou']
347 # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
348 # actual password
349 random_generator = random.SystemRandom()
302 random_chars = [350 random_chars = [
303 random.choice(alphanumeric_chars) for _ in range(length)]351 random_generator.choice(alphanumeric_chars) for _ in range(length)]
304 return(''.join(random_chars))352 return(''.join(random_chars))
305353
306354
307def list_nics(nic_type):355def list_nics(nic_type):
308 '''Return a list of nics of given type(s)'''356 '''Return a list of nics of given type(s)'''
309 if isinstance(nic_type, basestring):357 if isinstance(nic_type, six.string_types):
310 int_types = [nic_type]358 int_types = [nic_type]
311 else:359 else:
312 int_types = nic_type360 int_types = nic_type
313 interfaces = []361 interfaces = []
314 for int_type in int_types:362 for int_type in int_types:
315 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']363 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
316 ip_output = subprocess.check_output(cmd).split('\n')364 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
317 ip_output = (line for line in ip_output if line)365 ip_output = (line for line in ip_output if line)
318 for line in ip_output:366 for line in ip_output:
319 if line.split()[1].startswith(int_type):367 if line.split()[1].startswith(int_type):
320 interfaces.append(line.split()[1].replace(":", ""))368 matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
369 if matched:
370 interface = matched.groups()[0]
371 else:
372 interface = line.split()[1].replace(":", "")
373 interfaces.append(interface)
374
321 return interfaces375 return interfaces
322376
323377
@@ -329,7 +383,7 @@
329383
330def get_nic_mtu(nic):384def get_nic_mtu(nic):
331 cmd = ['ip', 'addr', 'show', nic]385 cmd = ['ip', 'addr', 'show', nic]
332 ip_output = subprocess.check_output(cmd).split('\n')386 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
333 mtu = ""387 mtu = ""
334 for line in ip_output:388 for line in ip_output:
335 words = line.split()389 words = line.split()
@@ -340,7 +394,7 @@
340394
341def get_nic_hwaddr(nic):395def get_nic_hwaddr(nic):
342 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]396 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
343 ip_output = subprocess.check_output(cmd)397 ip_output = subprocess.check_output(cmd).decode('UTF-8')
344 hwaddr = ""398 hwaddr = ""
345 words = ip_output.split()399 words = ip_output.split()
346 if 'link/ether' in words:400 if 'link/ether' in words:
@@ -355,10 +409,13 @@
355 * 0 => Installed revno is the same as supplied arg409 * 0 => Installed revno is the same as supplied arg
356 * -1 => Installed revno is less than supplied arg410 * -1 => Installed revno is less than supplied arg
357411
412 This function imports apt_cache function from charmhelpers.fetch if
413 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
414 you call this function, or pass an apt_pkg.Cache() instance.
358 '''415 '''
359 import apt_pkg416 import apt_pkg
360 from charmhelpers.fetch import apt_cache
361 if not pkgcache:417 if not pkgcache:
418 from charmhelpers.fetch import apt_cache
362 pkgcache = apt_cache()419 pkgcache = apt_cache()
363 pkg = pkgcache[package]420 pkg = pkgcache[package]
364 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)421 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
@@ -373,13 +430,21 @@
373 os.chdir(cur)430 os.chdir(cur)
374431
375432
376def chownr(path, owner, group):433def chownr(path, owner, group, follow_links=True):
377 uid = pwd.getpwnam(owner).pw_uid434 uid = pwd.getpwnam(owner).pw_uid
378 gid = grp.getgrnam(group).gr_gid435 gid = grp.getgrnam(group).gr_gid
436 if follow_links:
437 chown = os.chown
438 else:
439 chown = os.lchown
379440
380 for root, dirs, files in os.walk(path):441 for root, dirs, files in os.walk(path):
381 for name in dirs + files:442 for name in dirs + files:
382 full = os.path.join(root, name)443 full = os.path.join(root, name)
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)
384 if not broken_symlink:445 if not broken_symlink:
385 os.chown(full, uid, gid)446 chown(full, uid, gid)
447
448
449def lchownr(path, owner, group):
450 chownr(path, owner, group, follow_links=False)
386451
=== modified file 'lib/charmhelpers/core/services/__init__.py'
--- lib/charmhelpers/core/services/__init__.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/core/services/__init__.py 2015-09-08 18:07:26 +0000
@@ -1,2 +1,18 @@
1from .base import *1# Copyright 2014-2015 Canonical Limited.
2from .helpers import *2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from .base import * # NOQA
18from .helpers import * # NOQA
319
=== modified file 'lib/charmhelpers/core/services/base.py'
--- lib/charmhelpers/core/services/base.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/core/services/base.py 2015-09-08 18:07:26 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import re18import re
3import json19import json
420
=== modified file 'lib/charmhelpers/core/services/helpers.py'
--- lib/charmhelpers/core/services/helpers.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/core/services/helpers.py 2015-09-08 18:07:26 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import yaml18import yaml
3from charmhelpers.core import hookenv19from charmhelpers.core import hookenv
@@ -29,12 +45,14 @@
29 """45 """
30 name = None46 name = None
31 interface = None47 interface = None
32 required_keys = []
3348
34 def __init__(self, name=None, additional_required_keys=None):49 def __init__(self, name=None, additional_required_keys=None):
50 if not hasattr(self, 'required_keys'):
51 self.required_keys = []
52
35 if name is not None:53 if name is not None:
36 self.name = name54 self.name = name
37 if additional_required_keys is not None:55 if additional_required_keys:
38 self.required_keys.extend(additional_required_keys)56 self.required_keys.extend(additional_required_keys)
39 self.get_data()57 self.get_data()
4058
@@ -118,7 +136,10 @@
118 """136 """
119 name = 'db'137 name = 'db'
120 interface = 'mysql'138 interface = 'mysql'
121 required_keys = ['host', 'user', 'password', 'database']139
140 def __init__(self, *args, **kwargs):
141 self.required_keys = ['host', 'user', 'password', 'database']
142 RelationContext.__init__(self, *args, **kwargs)
122143
123144
124class HttpRelation(RelationContext):145class HttpRelation(RelationContext):
@@ -130,7 +151,10 @@
130 """151 """
131 name = 'website'152 name = 'website'
132 interface = 'http'153 interface = 'http'
133 required_keys = ['host', 'port']154
155 def __init__(self, *args, **kwargs):
156 self.required_keys = ['host', 'port']
157 RelationContext.__init__(self, *args, **kwargs)
134158
135 def provide_data(self):159 def provide_data(self):
136 return {160 return {
@@ -196,7 +220,7 @@
196 if not os.path.isabs(file_name):220 if not os.path.isabs(file_name):
197 file_name = os.path.join(hookenv.charm_dir(), file_name)221 file_name = os.path.join(hookenv.charm_dir(), file_name)
198 with open(file_name, 'w') as file_stream:222 with open(file_name, 'w') as file_stream:
199 os.fchmod(file_stream.fileno(), 0600)223 os.fchmod(file_stream.fileno(), 0o600)
200 yaml.dump(config_data, file_stream)224 yaml.dump(config_data, file_stream)
201225
202 def read_context(self, file_name):226 def read_context(self, file_name):
@@ -211,15 +235,19 @@
211235
212class TemplateCallback(ManagerCallback):236class TemplateCallback(ManagerCallback):
213 """237 """
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
215239 action.
216 :param str source: The template source file, relative to `$CHARM_DIR/templates`240
241 :param str source: The template source file, relative to
242 `$CHARM_DIR/templates`
243
217 :param str target: The target to write the rendered template to244 :param str target: The target to write the rendered template to
218 :param str owner: The owner of the rendered file245 :param str owner: The owner of the rendered file
219 :param str group: The group of the rendered file246 :param str group: The group of the rendered file
220 :param int perms: The permissions of the rendered file247 :param int perms: The permissions of the rendered file
221 """248 """
222 def __init__(self, source, target, owner='root', group='root', perms=0444):249 def __init__(self, source, target,
250 owner='root', group='root', perms=0o444):
223 self.source = source251 self.source = source
224 self.target = target252 self.target = target
225 self.owner = owner253 self.owner = owner
226254
=== added file 'lib/charmhelpers/core/strutils.py'
--- lib/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/core/strutils.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,42 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20import six
21
22
23def bool_from_string(value):
24 """Interpret string value as boolean.
25
26 Returns True if value translates to True otherwise False.
27 """
28 if isinstance(value, six.string_types):
29 value = six.text_type(value)
30 else:
31 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
32 raise ValueError(msg)
33
34 value = value.strip().lower()
35
36 if value in ['y', 'yes', 'true', 't', 'on']:
37 return True
38 elif value in ['n', 'no', 'false', 'f', 'off']:
39 return False
40
41 msg = "Unable to interpret string value '%s' as boolean" % (value)
42 raise ValueError(msg)
043
=== added file 'lib/charmhelpers/core/sysctl.py'
--- lib/charmhelpers/core/sysctl.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/core/sysctl.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,56 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20import yaml
21
22from subprocess import check_call
23
24from charmhelpers.core.hookenv import (
25 log,
26 DEBUG,
27 ERROR,
28)
29
30__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
31
32
33def create(sysctl_dict, sysctl_file):
34 """Creates a sysctl.conf file from a YAML associative array
35
36 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
37 :type sysctl_dict: str
38 :param sysctl_file: path to the sysctl file to be saved
39 :type sysctl_file: str or unicode
40 :returns: None
41 """
42 try:
43 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
44 except yaml.YAMLError:
45 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
46 level=ERROR)
47 return
48
49 with open(sysctl_file, "w") as fd:
50 for key, value in sysctl_dict_parsed.items():
51 fd.write("{}={}\n".format(key, value))
52
53 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
54 level=DEBUG)
55
56 check_call(["sysctl", "-p", sysctl_file])
057
=== modified file 'lib/charmhelpers/core/templating.py'
--- lib/charmhelpers/core/templating.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/core/templating.py 2015-09-08 18:07:26 +0000
@@ -1,10 +1,27 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
218
3from charmhelpers.core import host19from charmhelpers.core import host
4from charmhelpers.core import hookenv20from charmhelpers.core import hookenv
521
622
7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):23def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None, encoding='UTF-8'):
8 """25 """
9 Render a template.26 Render a template.
1027
@@ -47,5 +64,5 @@
47 level=hookenv.ERROR)64 level=hookenv.ERROR)
48 raise e65 raise e
49 content = template.render(context)66 content = template.render(context)
50 host.mkdir(os.path.dirname(target))67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
51 host.write_file(target, content, owner, group, perms)68 host.write_file(target, content.encode(encoding), owner, group, perms)
5269
=== added file 'lib/charmhelpers/core/unitdata.py'
--- lib/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/core/unitdata.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,477 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19#
20#
21# Authors:
22# Kapil Thangavelu <kapil.foss@gmail.com>
23#
24"""
25Intro
26-----
27
28A simple way to store state in units. This provides a key value
29storage with support for versioned, transactional operation,
30and can calculate deltas from previous values to simplify unit logic
31when processing changes.
32
33
34Hook Integration
35----------------
36
37There are several extant frameworks for hook execution, including
38
39 - charmhelpers.core.hookenv.Hooks
40 - charmhelpers.core.services.ServiceManager
41
42The storage classes are framework agnostic, one simple integration is
43via the HookData contextmanager. It will record the current hook
44execution environment (including relation data, config data, etc.),
45setup a transaction and allow easy access to the changes from
46previously seen values. One consequence of the integration is the
47reservation of particular keys ('rels', 'unit', 'env', 'config',
48'charm_revisions') for their respective values.
49
50Here's a fully worked integration example using hookenv.Hooks::
51
52 from charmhelper.core import hookenv, unitdata
53
54 hook_data = unitdata.HookData()
55 db = unitdata.kv()
56 hooks = hookenv.Hooks()
57
58 @hooks.hook
59 def config_changed():
60 # Print all changes to configuration from previously seen
61 # values.
62 for changed, (prev, cur) in hook_data.conf.items():
63 print('config changed', changed,
64 'previous value', prev,
65 'current value', cur)
66
67 # Get some unit specific bookeeping
68 if not db.get('pkg_key'):
69 key = urllib.urlopen('https://example.com/pkg_key').read()
70 db.set('pkg_key', key)
71
72 # Directly access all charm config as a mapping.
73 conf = db.getrange('config', True)
74
75 # Directly access all relation data as a mapping
76 rels = db.getrange('rels', True)
77
78 if __name__ == '__main__':
79 with hook_data():
80 hook.execute()
81
82
83A more basic integration is via the hook_scope context manager which simply
84manages transaction scope (and records hook name, and timestamp)::
85
86 >>> from unitdata import kv
87 >>> db = kv()
88 >>> with db.hook_scope('install'):
89 ... # do work, in transactional scope.
90 ... db.set('x', 1)
91 >>> db.get('x')
92 1
93
94
95Usage
96-----
97
98Values are automatically json de/serialized to preserve basic typing
99and complex data struct capabilities (dicts, lists, ints, booleans, etc).
100
101Individual values can be manipulated via get/set::
102
103 >>> kv.set('y', True)
104 >>> kv.get('y')
105 True
106
107 # We can set complex values (dicts, lists) as a single key.
108 >>> kv.set('config', {'a': 1, 'b': True'})
109
110 # Also supports returning dictionaries as a record which
111 # provides attribute access.
112 >>> config = kv.get('config', record=True)
113 >>> config.b
114 True
115
116
117Groups of keys can be manipulated with update/getrange::
118
119 >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
120 >>> kv.getrange('gui.', strip=True)
121 {'z': 1, 'y': 2}
122
123When updating values, its very helpful to understand which values
124have actually changed and how have they changed. The storage
125provides a delta method to provide for this::
126
127 >>> data = {'debug': True, 'option': 2}
128 >>> delta = kv.delta(data, 'config.')
129 >>> delta.debug.previous
130 None
131 >>> delta.debug.current
132 True
133 >>> delta
134 {'debug': (None, True), 'option': (None, 2)}
135
136Note the delta method does not persist the actual change, it needs to
137be explicitly saved via 'update' method::
138
139 >>> kv.update(data, 'config.')
140
141Values modified in the context of a hook scope retain historical values
142associated to the hookname.
143
144 >>> with db.hook_scope('config-changed'):
145 ... db.set('x', 42)
146 >>> db.gethistory('x')
147 [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
148 (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
149
150"""
151
152import collections
153import contextlib
154import datetime
155import json
156import os
157import pprint
158import sqlite3
159import sys
160
161__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
162
163
164class Storage(object):
165 """Simple key value database for local unit state within charms.
166
167 Modifications are automatically committed at hook exit. That's
168 currently regardless of exit code.
169
170 To support dicts, lists, integer, floats, and booleans values
171 are automatically json encoded/decoded.
172 """
173 def __init__(self, path=None):
174 self.db_path = path
175 if path is None:
176 self.db_path = os.path.join(
177 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
178 self.conn = sqlite3.connect('%s' % self.db_path)
179 self.cursor = self.conn.cursor()
180 self.revision = None
181 self._closed = False
182 self._init()
183
184 def close(self):
185 if self._closed:
186 return
187 self.flush(False)
188 self.cursor.close()
189 self.conn.close()
190 self._closed = True
191
192 def _scoped_query(self, stmt, params=None):
193 if params is None:
194 params = []
195 return stmt, params
196
197 def get(self, key, default=None, record=False):
198 self.cursor.execute(
199 *self._scoped_query(
200 'select data from kv where key=?', [key]))
201 result = self.cursor.fetchone()
202 if not result:
203 return default
204 if record:
205 return Record(json.loads(result[0]))
206 return json.loads(result[0])
207
208 def getrange(self, key_prefix, strip=False):
209 stmt = "select key, data from kv where key like '%s%%'" % key_prefix
210 self.cursor.execute(*self._scoped_query(stmt))
211 result = self.cursor.fetchall()
212
213 if not result:
214 return None
215 if not strip:
216 key_prefix = ''
217 return dict([
218 (k[len(key_prefix):], json.loads(v)) for k, v in result])
219
220 def update(self, mapping, prefix=""):
221 for k, v in mapping.items():
222 self.set("%s%s" % (prefix, k), v)
223
224 def unset(self, key):
225 self.cursor.execute('delete from kv where key=?', [key])
226 if self.revision and self.cursor.rowcount:
227 self.cursor.execute(
228 'insert into kv_revisions values (?, ?, ?)',
229 [key, self.revision, json.dumps('DELETED')])
230
231 def set(self, key, value):
232 serialized = json.dumps(value)
233
234 self.cursor.execute(
235 'select data from kv where key=?', [key])
236 exists = self.cursor.fetchone()
237
238 # Skip mutations to the same value
239 if exists:
240 if exists[0] == serialized:
241 return value
242
243 if not exists:
244 self.cursor.execute(
245 'insert into kv (key, data) values (?, ?)',
246 (key, serialized))
247 else:
248 self.cursor.execute('''
249 update kv
250 set data = ?
251 where key = ?''', [serialized, key])
252
253 # Save
254 if not self.revision:
255 return value
256
257 self.cursor.execute(
258 'select 1 from kv_revisions where key=? and revision=?',
259 [key, self.revision])
260 exists = self.cursor.fetchone()
261
262 if not exists:
263 self.cursor.execute(
264 '''insert into kv_revisions (
265 revision, key, data) values (?, ?, ?)''',
266 (self.revision, key, serialized))
267 else:
268 self.cursor.execute(
269 '''
270 update kv_revisions
271 set data = ?
272 where key = ?
273 and revision = ?''',
274 [serialized, key, self.revision])
275
276 return value
277
278 def delta(self, mapping, prefix):
279 """
280 return a delta containing values that have changed.
281 """
282 previous = self.getrange(prefix, strip=True)
283 if not previous:
284 pk = set()
285 else:
286 pk = set(previous.keys())
287 ck = set(mapping.keys())
288 delta = DeltaSet()
289
290 # added
291 for k in ck.difference(pk):
292 delta[k] = Delta(None, mapping[k])
293
294 # removed
295 for k in pk.difference(ck):
296 delta[k] = Delta(previous[k], None)
297
298 # changed
299 for k in pk.intersection(ck):
300 c = mapping[k]
301 p = previous[k]
302 if c != p:
303 delta[k] = Delta(p, c)
304
305 return delta
306
307 @contextlib.contextmanager
308 def hook_scope(self, name=""):
309 """Scope all future interactions to the current hook execution
310 revision."""
311 assert not self.revision
312 self.cursor.execute(
313 'insert into hooks (hook, date) values (?, ?)',
314 (name or sys.argv[0],
315 datetime.datetime.utcnow().isoformat()))
316 self.revision = self.cursor.lastrowid
317 try:
318 yield self.revision
319 self.revision = None
320 except:
321 self.flush(False)
322 self.revision = None
323 raise
324 else:
325 self.flush()
326
327 def flush(self, save=True):
328 if save:
329 self.conn.commit()
330 elif self._closed:
331 return
332 else:
333 self.conn.rollback()
334
335 def _init(self):
336 self.cursor.execute('''
337 create table if not exists kv (
338 key text,
339 data text,
340 primary key (key)
341 )''')
342 self.cursor.execute('''
343 create table if not exists kv_revisions (
344 key text,
345 revision integer,
346 data text,
347 primary key (key, revision)
348 )''')
349 self.cursor.execute('''
350 create table if not exists hooks (
351 version integer primary key autoincrement,
352 hook text,
353 date text
354 )''')
355 self.conn.commit()
356
357 def gethistory(self, key, deserialize=False):
358 self.cursor.execute(
359 '''
360 select kv.revision, kv.key, kv.data, h.hook, h.date
361 from kv_revisions kv,
362 hooks h
363 where kv.key=?
364 and kv.revision = h.version
365 ''', [key])
366 if deserialize is False:
367 return self.cursor.fetchall()
368 return map(_parse_history, self.cursor.fetchall())
369
370 def debug(self, fh=sys.stderr):
371 self.cursor.execute('select * from kv')
372 pprint.pprint(self.cursor.fetchall(), stream=fh)
373 self.cursor.execute('select * from kv_revisions')
374 pprint.pprint(self.cursor.fetchall(), stream=fh)
375
376
377def _parse_history(d):
378 return (d[0], d[1], json.loads(d[2]), d[3],
379 datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
380
381
382class HookData(object):
383 """Simple integration for existing hook exec frameworks.
384
385 Records all unit information, and stores deltas for processing
386 by the hook.
387
388 Sample::
389
390 from charmhelper.core import hookenv, unitdata
391
392 changes = unitdata.HookData()
393 db = unitdata.kv()
394 hooks = hookenv.Hooks()
395
396 @hooks.hook
397 def config_changed():
398 # View all changes to configuration
399 for changed, (prev, cur) in changes.conf.items():
400 print('config changed', changed,
401 'previous value', prev,
402 'current value', cur)
403
404 # Get some unit specific bookeeping
405 if not db.get('pkg_key'):
406 key = urllib.urlopen('https://example.com/pkg_key').read()
407 db.set('pkg_key', key)
408
409 if __name__ == '__main__':
410 with changes():
411 hook.execute()
412
413 """
414 def __init__(self):
415 self.kv = kv()
416 self.conf = None
417 self.rels = None
418
419 @contextlib.contextmanager
420 def __call__(self):
421 from charmhelpers.core import hookenv
422 hook_name = hookenv.hook_name()
423
424 with self.kv.hook_scope(hook_name):
425 self._record_charm_version(hookenv.charm_dir())
426 delta_config, delta_relation = self._record_hook(hookenv)
427 yield self.kv, delta_config, delta_relation
428
429 def _record_charm_version(self, charm_dir):
430 # Record revisions.. charm revisions are meaningless
431 # to charm authors as they don't control the revision.
432 # so logic dependnent on revision is not particularly
433 # useful, however it is useful for debugging analysis.
434 charm_rev = open(
435 os.path.join(charm_dir, 'revision')).read().strip()
436 charm_rev = charm_rev or '0'
437 revs = self.kv.get('charm_revisions', [])
438 if charm_rev not in revs:
439 revs.append(charm_rev.strip() or '0')
440 self.kv.set('charm_revisions', revs)
441
442 def _record_hook(self, hookenv):
443 data = hookenv.execution_environment()
444 self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
445 self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
446 self.kv.set('env', dict(data['env']))
447 self.kv.set('unit', data['unit'])
448 self.kv.set('relid', data.get('relid'))
449 return conf_delta, rels_delta
450
451
452class Record(dict):
453
454 __slots__ = ()
455
456 def __getattr__(self, k):
457 if k in self:
458 return self[k]
459 raise AttributeError(k)
460
461
462class DeltaSet(Record):
463
464 __slots__ = ()
465
466
467Delta = collections.namedtuple('Delta', ['previous', 'current'])
468
469
470_KV = None
471
472
473def kv():
474 global _KV
475 if _KV is None:
476 _KV = Storage()
477 return _KV
0478
=== modified file 'lib/charmhelpers/fetch/__init__.py'
--- lib/charmhelpers/fetch/__init__.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/fetch/__init__.py 2015-09-08 18:07:26 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import importlib17import importlib
2from tempfile import NamedTemporaryFile18from tempfile import NamedTemporaryFile
3import time19import time
@@ -5,10 +21,6 @@
5from charmhelpers.core.host import (21from charmhelpers.core.host import (
6 lsb_release22 lsb_release
7)23)
8from urlparse import (
9 urlparse,
10 urlunparse,
11)
12import subprocess24import subprocess
13from charmhelpers.core.hookenv import (25from charmhelpers.core.hookenv import (
14 config,26 config,
@@ -16,6 +28,12 @@
16)28)
17import os29import os
1830
31import six
32if six.PY3:
33 from urllib.parse import urlparse, urlunparse
34else:
35 from urlparse import urlparse, urlunparse
36
1937
20CLOUD_ARCHIVE = """# Ubuntu Cloud Archive38CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
21deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main39deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
@@ -62,9 +80,16 @@
62 'trusty-juno/updates': 'trusty-updates/juno',80 'trusty-juno/updates': 'trusty-updates/juno',
63 'trusty-updates/juno': 'trusty-updates/juno',81 'trusty-updates/juno': 'trusty-updates/juno',
64 'juno/proposed': 'trusty-proposed/juno',82 'juno/proposed': 'trusty-proposed/juno',
65 'juno/proposed': 'trusty-proposed/juno',
66 'trusty-juno/proposed': 'trusty-proposed/juno',83 'trusty-juno/proposed': 'trusty-proposed/juno',
67 'trusty-proposed/juno': 'trusty-proposed/juno',84 'trusty-proposed/juno': 'trusty-proposed/juno',
85 # Kilo
86 'kilo': 'trusty-updates/kilo',
87 'trusty-kilo': 'trusty-updates/kilo',
88 'trusty-kilo/updates': 'trusty-updates/kilo',
89 'trusty-updates/kilo': 'trusty-updates/kilo',
90 'kilo/proposed': 'trusty-proposed/kilo',
91 'trusty-kilo/proposed': 'trusty-proposed/kilo',
92 'trusty-proposed/kilo': 'trusty-proposed/kilo',
68}93}
6994
70# The order of this list is very important. Handlers should be listed in from95# The order of this list is very important. Handlers should be listed in from
@@ -72,6 +97,7 @@
72FETCH_HANDLERS = (97FETCH_HANDLERS = (
73 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',98 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
74 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',99 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
100 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
75)101)
76102
77APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.103APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
@@ -148,7 +174,7 @@
148 cmd = ['apt-get', '--assume-yes']174 cmd = ['apt-get', '--assume-yes']
149 cmd.extend(options)175 cmd.extend(options)
150 cmd.append('install')176 cmd.append('install')
151 if isinstance(packages, basestring):177 if isinstance(packages, six.string_types):
152 cmd.append(packages)178 cmd.append(packages)
153 else:179 else:
154 cmd.extend(packages)180 cmd.extend(packages)
@@ -181,7 +207,7 @@
181def apt_purge(packages, fatal=False):207def apt_purge(packages, fatal=False):
182 """Purge one or more packages"""208 """Purge one or more packages"""
183 cmd = ['apt-get', '--assume-yes', 'purge']209 cmd = ['apt-get', '--assume-yes', 'purge']
184 if isinstance(packages, basestring):210 if isinstance(packages, six.string_types):
185 cmd.append(packages)211 cmd.append(packages)
186 else:212 else:
187 cmd.extend(packages)213 cmd.extend(packages)
@@ -192,7 +218,7 @@
192def apt_hold(packages, fatal=False):218def apt_hold(packages, fatal=False):
193 """Hold one or more packages"""219 """Hold one or more packages"""
194 cmd = ['apt-mark', 'hold']220 cmd = ['apt-mark', 'hold']
195 if isinstance(packages, basestring):221 if isinstance(packages, six.string_types):
196 cmd.append(packages)222 cmd.append(packages)
197 else:223 else:
198 cmd.extend(packages)224 cmd.extend(packages)
@@ -208,7 +234,8 @@
208 """Add a package source to this system.234 """Add a package source to this system.
209235
210 @param source: a URL or sources.list entry, as supported by236 @param source: a URL or sources.list entry, as supported by
211 add-apt-repository(1). Examples:237 add-apt-repository(1). Examples::
238
212 ppa:charmers/example239 ppa:charmers/example
213 deb https://stub:key@private.example.com/ubuntu trusty main240 deb https://stub:key@private.example.com/ubuntu trusty main
214241
@@ -217,6 +244,7 @@
217 pocket for the release.244 pocket for the release.
218 'cloud:' may be used to activate official cloud archive pockets,245 'cloud:' may be used to activate official cloud archive pockets,
219 such as 'cloud:icehouse'246 such as 'cloud:icehouse'
247 'distro' may be used as a noop
220248
221 @param key: A key to be added to the system's APT keyring and used249 @param key: A key to be added to the system's APT keyring and used
222 to verify the signatures on packages. Ideally, this should be an250 to verify the signatures on packages. Ideally, this should be an
@@ -250,12 +278,14 @@
250 release = lsb_release()['DISTRIB_CODENAME']278 release = lsb_release()['DISTRIB_CODENAME']
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:
252 apt.write(PROPOSED_POCKET.format(release))280 apt.write(PROPOSED_POCKET.format(release))
281 elif source == 'distro':
282 pass
253 else:283 else:
254 raise SourceConfigError("Unknown source: {!r}".format(source))284 log("Unknown source: {!r}".format(source))
255285
256 if key:286 if key:
257 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:287 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
258 with NamedTemporaryFile() as key_file:288 with NamedTemporaryFile('w+') as key_file:
259 key_file.write(key)289 key_file.write(key)
260 key_file.flush()290 key_file.flush()
261 key_file.seek(0)291 key_file.seek(0)
@@ -292,14 +322,14 @@
292 sources = safe_load((config(sources_var) or '').strip()) or []322 sources = safe_load((config(sources_var) or '').strip()) or []
293 keys = safe_load((config(keys_var) or '').strip()) or None323 keys = safe_load((config(keys_var) or '').strip()) or None
294324
295 if isinstance(sources, basestring):325 if isinstance(sources, six.string_types):
296 sources = [sources]326 sources = [sources]
297327
298 if keys is None:328 if keys is None:
299 for source in sources:329 for source in sources:
300 add_source(source, None)330 add_source(source, None)
301 else:331 else:
302 if isinstance(keys, basestring):332 if isinstance(keys, six.string_types):
303 keys = [keys]333 keys = [keys]
304334
305 if len(sources) != len(keys):335 if len(sources) != len(keys):
@@ -396,7 +426,7 @@
396 while result is None or result == APT_NO_LOCK:426 while result is None or result == APT_NO_LOCK:
397 try:427 try:
398 result = subprocess.check_call(cmd, env=env)428 result = subprocess.check_call(cmd, env=env)
399 except subprocess.CalledProcessError, e:429 except subprocess.CalledProcessError as e:
400 retry_count = retry_count + 1430 retry_count = retry_count + 1
401 if retry_count > APT_NO_LOCK_RETRY_COUNT:431 if retry_count > APT_NO_LOCK_RETRY_COUNT:
402 raise432 raise
403433
=== modified file 'lib/charmhelpers/fetch/archiveurl.py'
--- lib/charmhelpers/fetch/archiveurl.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/fetch/archiveurl.py 2015-09-08 18:07:26 +0000
@@ -1,8 +1,22 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import urllib2
3from urllib import urlretrieve
4import urlparse
5import hashlib18import hashlib
19import re
620
7from charmhelpers.fetch import (21from charmhelpers.fetch import (
8 BaseFetchHandler,22 BaseFetchHandler,
@@ -14,6 +28,41 @@
14)28)
15from charmhelpers.core.host import mkdir, check_hash29from charmhelpers.core.host import mkdir, check_hash
1630
31import six
32if six.PY3:
33 from urllib.request import (
34 build_opener, install_opener, urlopen, urlretrieve,
35 HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
36 )
37 from urllib.parse import urlparse, urlunparse, parse_qs
38 from urllib.error import URLError
39else:
40 from urllib import urlretrieve
41 from urllib2 import (
42 build_opener, install_opener, urlopen,
43 HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
44 URLError
45 )
46 from urlparse import urlparse, urlunparse, parse_qs
47
48
49def splituser(host):
50 '''urllib.splituser(), but six's support of this seems broken'''
51 _userprog = re.compile('^(.*)@(.*)$')
52 match = _userprog.match(host)
53 if match:
54 return match.group(1, 2)
55 return None, host
56
57
58def splitpasswd(user):
59 '''urllib.splitpasswd(), but six's support of this is missing'''
60 _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
61 match = _passwdprog.match(user)
62 if match:
63 return match.group(1, 2)
64 return user, None
65
1766
18class ArchiveUrlFetchHandler(BaseFetchHandler):67class ArchiveUrlFetchHandler(BaseFetchHandler):
19 """68 """
@@ -42,20 +91,20 @@
42 """91 """
43 # propogate all exceptions92 # propogate all exceptions
44 # URLError, OSError, etc93 # URLError, OSError, etc
45 proto, netloc, path, params, query, fragment = urlparse.urlparse(source)94 proto, netloc, path, params, query, fragment = urlparse(source)
46 if proto in ('http', 'https'):95 if proto in ('http', 'https'):
47 auth, barehost = urllib2.splituser(netloc)96 auth, barehost = splituser(netloc)
48 if auth is not None:97 if auth is not None:
49 source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))98 source = urlunparse((proto, barehost, path, params, query, fragment))
50 username, password = urllib2.splitpasswd(auth)99 username, password = splitpasswd(auth)
51 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()100 passman = HTTPPasswordMgrWithDefaultRealm()
52 # Realm is set to None in add_password to force the username and password101 # Realm is set to None in add_password to force the username and password
53 # to be used whatever the realm102 # to be used whatever the realm
54 passman.add_password(None, source, username, password)103 passman.add_password(None, source, username, password)
55 authhandler = urllib2.HTTPBasicAuthHandler(passman)104 authhandler = HTTPBasicAuthHandler(passman)
56 opener = urllib2.build_opener(authhandler)105 opener = build_opener(authhandler)
57 urllib2.install_opener(opener)106 install_opener(opener)
58 response = urllib2.urlopen(source)107 response = urlopen(source)
59 try:108 try:
60 with open(dest, 'w') as dest_file:109 with open(dest, 'w') as dest_file:
61 dest_file.write(response.read())110 dest_file.write(response.read())
@@ -74,33 +123,38 @@
74 """123 """
75 Download and install an archive file, with optional checksum validation.124 Download and install an archive file, with optional checksum validation.
76125
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.
78 For example::127 For example::
79128
80 handler.install('http://example.com/file.tgz#sha1=deadbeef')129 handler.install('http://example.com/file.tgz#sha1=deadbeef')
81130
82 :param str source: URL pointing to an archive file.131 :param str source: URL pointing to an archive file.
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,
84 installs to `$CHARM_DIR/archives/archive_file_name`.133 installs to `$CHARM_DIR/archives/archive_file_name`.
85 :param str checksum: If given, validate the archive file after download.134 :param str checksum: If given, validate the archive file after download.
86 :param str hash_type: Algorithm used to generate :param:`checksum`.135 :param str hash_type: Algorithm used to generate `checksum`.
87 Can be any hash alrgorithm supported by :mod:`hashlib`,136 Can be any hash alrgorithm supported by :mod:`hashlib`,
88 such as md5, sha1, sha256, sha512, etc.137 such as md5, sha1, sha256, sha512, etc.
138
89 """139 """
90 url_parts = self.parse_url(source)140 url_parts = self.parse_url(source)
91 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')141 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
92 if not os.path.exists(dest_dir):142 if not os.path.exists(dest_dir):
93 mkdir(dest_dir, perms=0755)143 mkdir(dest_dir, perms=0o755)
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))
95 try:145 try:
96 self.download(source, dld_file)146 self.download(source, dld_file)
97 except urllib2.URLError as e:147 except URLError as e:
98 raise UnhandledSource(e.reason)148 raise UnhandledSource(e.reason)
99 except OSError as e:149 except OSError as e:
100 raise UnhandledSource(e.strerror)150 raise UnhandledSource(e.strerror)
101 options = urlparse.parse_qs(url_parts.fragment)151 options = parse_qs(url_parts.fragment)
102 for key, value in options.items():152 for key, value in options.items():
103 if key in hashlib.algorithms:153 if not six.PY3:
154 algorithms = hashlib.algorithms
155 else:
156 algorithms = hashlib.algorithms_available
157 if key in algorithms:
104 check_hash(dld_file, value, key)158 check_hash(dld_file, value, key)
105 if checksum:159 if checksum:
106 check_hash(dld_file, checksum, hash_type)160 check_hash(dld_file, checksum, hash_type)
107161
=== modified file 'lib/charmhelpers/fetch/bzrurl.py'
--- lib/charmhelpers/fetch/bzrurl.py 2014-07-17 16:38:17 +0000
+++ lib/charmhelpers/fetch/bzrurl.py 2015-09-08 18:07:26 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2from charmhelpers.fetch import (18from charmhelpers.fetch import (
3 BaseFetchHandler,19 BaseFetchHandler,
@@ -5,12 +21,18 @@
5)21)
6from charmhelpers.core.host import mkdir22from charmhelpers.core.host import mkdir
723
24import six
25if six.PY3:
26 raise ImportError('bzrlib does not support Python3')
27
8try:28try:
9 from bzrlib.branch import Branch29 from bzrlib.branch import Branch
30 from bzrlib import bzrdir, workingtree, errors
10except ImportError:31except ImportError:
11 from charmhelpers.fetch import apt_install32 from charmhelpers.fetch import apt_install
12 apt_install("python-bzrlib")33 apt_install("python-bzrlib")
13 from bzrlib.branch import Branch34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
1436
1537
16class BzrUrlFetchHandler(BaseFetchHandler):38class BzrUrlFetchHandler(BaseFetchHandler):
@@ -31,8 +53,14 @@
31 from bzrlib.plugin import load_plugins53 from bzrlib.plugin import load_plugins
32 load_plugins()54 load_plugins()
33 try:55 try:
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
57 except errors.AlreadyControlDirError:
58 local_branch = Branch.open(dest)
59 try:
34 remote_branch = Branch.open(source)60 remote_branch = Branch.open(source)
35 remote_branch.bzrdir.sprout(dest).open_branch()61 remote_branch.push(local_branch)
62 tree = workingtree.WorkingTree.open(dest)
63 tree.update()
36 except Exception as e:64 except Exception as e:
37 raise e65 raise e
3866
@@ -42,7 +70,7 @@
42 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",70 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
43 branch_name)71 branch_name)
44 if not os.path.exists(dest_dir):72 if not os.path.exists(dest_dir):
45 mkdir(dest_dir, perms=0755)73 mkdir(dest_dir, perms=0o755)
46 try:74 try:
47 self.branch(source, dest_dir)75 self.branch(source, dest_dir)
48 except OSError as e:76 except OSError as e:
4977
=== added file 'lib/charmhelpers/fetch/giturl.py'
--- lib/charmhelpers/fetch/giturl.py 1970-01-01 00:00:00 +0000
+++ lib/charmhelpers/fetch/giturl.py 2015-09-08 18:07:26 +0000
@@ -0,0 +1,71 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18from charmhelpers.fetch import (
19 BaseFetchHandler,
20 UnhandledSource
21)
22from charmhelpers.core.host import mkdir
23
24import six
25if six.PY3:
26 raise ImportError('GitPython does not support Python 3')
27
28try:
29 from git import Repo
30except ImportError:
31 from charmhelpers.fetch import apt_install
32 apt_install("python-git")
33 from git import Repo
34
35from git.exc import GitCommandError # noqa E402
36
37
38class GitUrlFetchHandler(BaseFetchHandler):
39 """Handler for git branches via generic and github URLs"""
40 def can_handle(self, source):
41 url_parts = self.parse_url(source)
42 # TODO (mattyw) no support for ssh git@ yet
43 if url_parts.scheme not in ('http', 'https', 'git'):
44 return False
45 else:
46 return True
47
48 def clone(self, source, dest, branch):
49 if not self.can_handle(source):
50 raise UnhandledSource("Cannot handle {}".format(source))
51
52 repo = Repo.clone_from(source, dest)
53 repo.git.checkout(branch)
54
55 def install(self, source, branch="master", dest=None):
56 url_parts = self.parse_url(source)
57 branch_name = url_parts.path.strip("/").split("/")[-1]
58 if dest:
59 dest_dir = os.path.join(dest, branch_name)
60 else:
61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
62 branch_name)
63 if not os.path.exists(dest_dir):
64 mkdir(dest_dir, perms=0o755)
65 try:
66 self.clone(source, dest_dir, branch)
67 except GitCommandError as e:
68 raise UnhandledSource(e.message)
69 except OSError as e:
70 raise UnhandledSource(e.strerror)
71 return dest_dir
072
=== modified file 'lib/charmhelpers/payload/__init__.py'
--- lib/charmhelpers/payload/__init__.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/payload/__init__.py 2015-09-08 18:07:26 +0000
@@ -1,1 +1,17 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
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."
218
=== modified file 'lib/charmhelpers/payload/archive.py'
--- lib/charmhelpers/payload/archive.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/payload/archive.py 2015-09-08 18:07:26 +0000
@@ -1,3 +1,19 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
1import os17import os
2import tarfile18import tarfile
3import zipfile19import zipfile
420
=== modified file 'lib/charmhelpers/payload/execd.py'
--- lib/charmhelpers/payload/execd.py 2014-09-23 12:09:14 +0000
+++ lib/charmhelpers/payload/execd.py 2015-09-08 18:07:26 +0000
@@ -1,5 +1,21 @@
1#!/usr/bin/env python1#!/usr/bin/env python
22
3# Copyright 2014-2015 Canonical Limited.
4#
5# This file is part of charm-helpers.
6#
7# charm-helpers is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3 as
9# published by the Free Software Foundation.
10#
11# charm-helpers is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
3import os19import os
4import sys20import sys
5import subprocess21import subprocess
622
=== modified file 'metadata.yaml'
--- metadata.yaml 2015-04-17 08:37:59 +0000
+++ metadata.yaml 2015-09-08 18:07:26 +0000
@@ -16,6 +16,9 @@
16 interface: elasticsearch16 interface: elasticsearch
17 lumberjack:17 lumberjack:
18 interface: http18 interface: http
19 nrpe-external-master:
20 interface: nrpe-external-master
21 scope: container
19requires:22requires:
20 client:23 client:
21 interface: elasticsearch24 interface: elasticsearch

Subscribers

People subscribed via source and target branches