Merge lp:~sajoupa/charms/trusty/wordpress-services/wordpress-services-merge-with-is-charms into lp:~canonical-sysadmins/charms/trusty/wordpress-services/trunk

Proposed by Laurent Sesques on 2016-04-05
Status: Merged
Merged at revision: 109
Proposed branch: lp:~sajoupa/charms/trusty/wordpress-services/wordpress-services-merge-with-is-charms
Merge into: lp:~canonical-sysadmins/charms/trusty/wordpress-services/trunk
Diff against target: 3469 lines (+2091/-370)
29 files modified
Makefile (+15/-0)
config.yaml (+37/-0)
files/wp-upgrade-check.php (+29/-0)
hooks/actions.py (+36/-6)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+111/-23)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/fstab.py (+4/-4)
hooks/charmhelpers/core/hookenv.py (+484/-43)
hooks/charmhelpers/core/host.py (+316/-67)
hooks/charmhelpers/core/hugepage.py (+71/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/base.py (+43/-19)
hooks/charmhelpers/core/services/helpers.py (+43/-10)
hooks/charmhelpers/core/strutils.py (+72/-0)
hooks/charmhelpers/core/sysctl.py (+13/-7)
hooks/charmhelpers/core/templating.py (+21/-8)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/fetch/__init__.py (+41/-16)
hooks/charmhelpers/fetch/archiveurl.py (+18/-12)
hooks/charmhelpers/fetch/bzrurl.py (+27/-33)
hooks/charmhelpers/fetch/giturl.py (+24/-25)
hooks/install (+7/-0)
hooks/services.py (+7/-27)
hooks/wp_helpers.py (+22/-4)
metadata.yaml (+2/-1)
templates/wp-apparmor.j2 (+1/-0)
templates/wp-info.php.j2 (+13/-0)
templates/wp-nagios.j2 (+0/-48)
templates/wp-nrpe.j2 (+0/-17)
To merge this branch: bzr merge lp:~sajoupa/charms/trusty/wordpress-services/wordpress-services-merge-with-is-charms
Reviewer Review Type Date Requested Status
Thomas Cuthbert 2016-04-05 Pending
Review via email: mp+291000@code.launchpad.net
To post a comment you must log in.
Laurent Sesques (sajoupa) wrote :

Also, includes my fix for "INFO install E: Unable to locate package php-cli" (rev 108.2.15 - see https://code.launchpad.net/~sajoupa/canonical-is-charms/wordpress-services-fix-lsb-check/+merge/290981)

Thomas Cuthbert (tcuthbert) wrote :

This looks fine to me, I ran it in mojo-ci with no issues. Are you able to provide a summary of what is changing and I'll approve it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'Makefile'
2--- Makefile 1970-01-01 00:00:00 +0000
3+++ Makefile 2016-04-05 14:45:53 +0000
4@@ -0,0 +1,15 @@
5+#!/usr/bin/make
6+HOOKS_DIR := $(PWD)/hooks
7+TEST_PREFIX := PYTHONPATH=$(HOOKS_DIR)
8+
9+lint:
10+ flake8 --exclude hooks/charmhelpers --ignore=E501 hooks tests
11+ @charm proof
12+
13+bin/charm_helpers_sync.py:
14+ @mkdir -p bin
15+ @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
16+ > bin/charm_helpers_sync.py
17+
18+sync: bin/charm_helpers_sync.py
19+ @python bin/charm_helpers_sync.py -c charm-helpers.yaml
20
21=== modified file 'config.yaml'
22--- config.yaml 2015-02-25 17:05:27 +0000
23+++ config.yaml 2016-04-05 14:45:53 +0000
24@@ -46,6 +46,12 @@
25
26 If you're running multiple environments with the same services in them
27 this allows you to differentiate between them.
28+ nagios_servicegroups:
29+ default: ""
30+ type: string
31+ description: |
32+ A comma-separated list of nagios servicegroups.
33+ If left empty, the nagios_context will be used as the servicegroup
34 nagios_check_string:
35 default: "Proudly powered by WordPress"
36 type: string
37@@ -77,3 +83,34 @@
38
39 If admin_password is not provided it will be automatically generated
40 and stored on wordpress unit in the charm directory
41+ outbound_http_proxy:
42+ default: ""
43+ type: string
44+ description: >
45+ Optional URL specifying a "forward" proxy to allow wordpress and its
46+ plugins access to the Web. As an example:
47+
48+ outbound_http_proxy: http://user:pass@squid.example.com:3128/
49+ redirects:
50+ default: ""
51+ type: string
52+ description: >
53+ Optional YAML formatted list of redirects that will be added to
54+ apache vhost. For example setting this optino to:
55+
56+ [{"match": "(.*)\.gif$", "target": "http://example.com$1.jpg"},
57+ {"match": "/old", "target": "http://example.com/new/", "type": "permanent"}]
58+
59+ Will result in the following configuration stanzas if apache2-subordinate is used:
60+ RedirectMatch (.*)\.gif$ http://example.com$1.jpg
61+ RedirectMatch permanent /old http://example.com/new
62+ vhost_options:
63+ default: ""
64+ type: string
65+ description: >
66+ Optional YAML formatted list of additional virtual host config directives.
67+ For example:
68+
69+ [{"Header": "append Cache-Control \"proxy-revalidate\""},
70+ {"Header": "unset ETag"},
71+ {"ExpiresDefault": "\"access plus 1 days\""}]
72
73=== added file 'files/wp-upgrade-check.php'
74--- files/wp-upgrade-check.php 1970-01-01 00:00:00 +0000
75+++ files/wp-upgrade-check.php 2016-04-05 14:45:53 +0000
76@@ -0,0 +1,29 @@
77+<?php
78+
79+require_once('wp-load.php');
80+
81+global $wp_version;
82+$core_updates = 0;
83+
84+wp_version_check();
85+
86+$core = get_site_transient('update_core');
87+
88+foreach($core->updates as $update) {
89+ if($update->current != $wp_version) {
90+ $core_updates++;
91+ }
92+}
93+
94+if($core_updates) {
95+ print("CRITICAL : $core_updates core updates available !\n");
96+ exit(2);
97+} else {
98+ print("OK : no core update available\n");
99+ exit(0);
100+}
101+
102+print("CRITICAL : Error in " . __FILE__ . "\n");
103+exit(2);
104+
105+?>
106
107=== modified file 'hooks/actions.py'
108--- hooks/actions.py 2015-02-25 17:05:27 +0000
109+++ hooks/actions.py 2016-04-05 14:45:53 +0000
110@@ -30,6 +30,8 @@
111 host.rsync(upstream_code + '/',
112 config['install_path'],
113 options=['--executability']) # Because we don't want --delete
114+ host.rsync('files/wp-upgrade-check.php',
115+ os.path.join(config['install_path'], 'wp-upgrade-check.php'))
116 host.mkdir('{}/wp-content/uploads'.format(config['install_path']),
117 owner='www-data', perms=0755)
118 if wp_helpers.wordpress_configured():
119@@ -38,7 +40,10 @@
120
121
122 def install_packages(service_name):
123- packages = ['php5-cli', 'php5-mysql', 'php-symfony-yaml']
124+ if host.lsb_release()['DISTRIB_CODENAME'] == 'trusty':
125+ packages = ['php5-cli', 'php5-mysql', 'php-symfony-yaml', 'php5-curl']
126+ else:
127+ packages = ['php-cli', 'php-mysql', 'php-symfony-yaml', 'php-curl', 'libapache2-mod-php']
128 fetch.apt_update()
129 fetch.apt_install(packages)
130
131@@ -55,7 +60,7 @@
132 stdin=subprocess.PIPE,
133 stdout=subprocess.PIPE,
134 stderr=subprocess.STDOUT,
135- )
136+ )
137 return process.communicate(stdin)[0] # spit back stdout+stderr combined
138
139
140@@ -122,7 +127,7 @@
141 """Perform initial configuratin of wordpress if needed."""
142 config = hookenv.config()
143 if wp_helpers.wordpress_configured() or not config['initial_settings']:
144- hookenv.log('No initial_setting provided or wordprass already '
145+ hookenv.log('No initial_setting provided or wordpress already '
146 'configured. Skipping first install.')
147 return
148 hookenv.log('Starting wordpress initial configuration')
149@@ -183,7 +188,7 @@
150 def write_nrpe_checks(service_name):
151 config = hookenv.config()
152 relation = wp_helpers.NEMRelation()['nrpe-external-master'][0]
153- nrpe = NRPE(hostname=relation['nagios_hostname'])
154+ nrpe = NRPE(hostname=relation['nagios_hostname'], primary=True)
155
156 nrpe.add_check(
157 shortname='wordpress_http',
158@@ -198,11 +203,36 @@
159 check_cmd='check_http -I localhost -H {} -p {} -S'.format(
160 config['blog_hostname'], config['ssl_port_number'])
161 )
162+ nrpe.add_check(
163+ shortname='wordpress_upgrades',
164+ description='Check Wordpress core upgrades',
165+ check_cmd='/usr/bin/php {}/wp-upgrade-check.php'.format(
166+ config['install_path'])
167+ )
168+
169+ plugin_relations = wp_helpers.PluginRelation()['wordpress-plugin']
170+ for plugin in plugin_relations:
171+ plugin_name = plugin.get('plugin_name')
172+ if plugin_name:
173+ nrpe.add_check(
174+ shortname='wordpress_plugin_{}'.format(plugin_name),
175+ description='Check Wordpress Plugin {}'.format(plugin_name),
176+ check_cmd='check_file_age -w 31536000 -c 33696000 -f '
177+ '{}/wp-content/plugins/{}'
178+ ''.format(config['install_path'], plugin_name)
179+
180+ )
181+
182 nrpe.write()
183
184
185 def wipe_nrpe_checks(service_name):
186- os.unlink('/var/lib/nagios/export/{}.cfg'.format(hookenv.local_unit()))
187+ for f in glob.glob('/var/lib/nagios/export/service__*wordpress_http?.cfg'):
188+ if os.path.isfile(f):
189+ os.unlink(f)
190+ for f in glob.glob('/var/lib/nagios/export/service__*wordpress_plugin*.cfg'):
191+ if os.path.isfile(f):
192+ os.unlink(f)
193
194
195 def apparmor_dirs(service_name):
196@@ -234,7 +264,7 @@
197 options were changed
198 """
199 config = hookenv.config()
200- options = ['port_number', 'ssl_enabled', 'ssl_port_number',
201+ options = ['port_number', 'ssl_enabled', 'ssl_port_number', 'redirects',
202 'install_path', 'blog_hostname', 'additional_hostnames']
203 if not (wp_helpers.object_storage()
204 or any(config.changed(option) for option in options)):
205
206=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
207--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-01-27 14:54:02 +0000
208+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-04-05 14:45:53 +0000
209@@ -24,6 +24,8 @@
210 import pwd
211 import grp
212 import os
213+import glob
214+import shutil
215 import re
216 import shlex
217 import yaml
218@@ -108,6 +110,13 @@
219 # def local_monitors_relation_changed():
220 # update_nrpe_config()
221 #
222+# 4.a If your charm is a subordinate charm set primary=False
223+#
224+# from charmsupport.nrpe import NRPE
225+# (...)
226+# def update_nrpe_config():
227+# nrpe_compat = NRPE(primary=False)
228+#
229 # 5. ln -s hooks.py nrpe-external-master-relation-changed
230 # ln -s hooks.py local-monitors-relation-changed
231
232@@ -146,6 +155,13 @@
233 self.description = description
234 self.check_cmd = self._locate_cmd(check_cmd)
235
236+ def _get_check_filename(self):
237+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
238+
239+ def _get_service_filename(self, hostname):
240+ return os.path.join(NRPE.nagios_exportdir,
241+ 'service__{}_{}.cfg'.format(hostname, self.command))
242+
243 def _locate_cmd(self, check_cmd):
244 search_path = (
245 '/usr/lib/nagios/plugins',
246@@ -161,9 +177,21 @@
247 log('Check command not found: {}'.format(parts[0]))
248 return ''
249
250- def write(self, nagios_context, hostname, nagios_servicegroups=None):
251- nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
252- self.command)
253+ def _remove_service_files(self):
254+ if not os.path.exists(NRPE.nagios_exportdir):
255+ return
256+ for f in os.listdir(NRPE.nagios_exportdir):
257+ if f.endswith('_{}.cfg'.format(self.command)):
258+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
259+
260+ def remove(self, hostname):
261+ nrpe_check_file = self._get_check_filename()
262+ if os.path.exists(nrpe_check_file):
263+ os.remove(nrpe_check_file)
264+ self._remove_service_files()
265+
266+ def write(self, nagios_context, hostname, nagios_servicegroups):
267+ nrpe_check_file = self._get_check_filename()
268 with open(nrpe_check_file, 'w') as nrpe_check_config:
269 nrpe_check_config.write("# check {}\n".format(self.shortname))
270 nrpe_check_config.write("command[{}]={}\n".format(
271@@ -177,13 +205,8 @@
272 nagios_servicegroups)
273
274 def write_service_config(self, nagios_context, hostname,
275- nagios_servicegroups=None):
276- for f in os.listdir(NRPE.nagios_exportdir):
277- if re.search('.*{}.cfg'.format(self.command), f):
278- os.remove(os.path.join(NRPE.nagios_exportdir, f))
279-
280- if not nagios_servicegroups:
281- nagios_servicegroups = nagios_context
282+ nagios_servicegroups):
283+ self._remove_service_files()
284
285 templ_vars = {
286 'nagios_hostname': hostname,
287@@ -193,8 +216,7 @@
288 'command': self.command,
289 }
290 nrpe_service_text = Check.service_template.format(**templ_vars)
291- nrpe_service_file = '{}/service__{}_{}.cfg'.format(
292- NRPE.nagios_exportdir, hostname, self.command)
293+ nrpe_service_file = self._get_service_filename(hostname)
294 with open(nrpe_service_file, 'w') as nrpe_service_config:
295 nrpe_service_config.write(str(nrpe_service_text))
296
297@@ -207,24 +229,51 @@
298 nagios_exportdir = '/var/lib/nagios/export'
299 nrpe_confdir = '/etc/nagios/nrpe.d'
300
301- def __init__(self, hostname=None):
302+ def __init__(self, hostname=None, primary=True):
303 super(NRPE, self).__init__()
304 self.config = config()
305+ self.primary = primary
306 self.nagios_context = self.config['nagios_context']
307- if 'nagios_servicegroups' in self.config:
308+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
309 self.nagios_servicegroups = self.config['nagios_servicegroups']
310 else:
311- self.nagios_servicegroups = 'juju'
312+ self.nagios_servicegroups = self.nagios_context
313 self.unit_name = local_unit().replace('/', '-')
314 if hostname:
315 self.hostname = hostname
316 else:
317- self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
318+ nagios_hostname = get_nagios_hostname()
319+ if nagios_hostname:
320+ self.hostname = nagios_hostname
321+ else:
322+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
323 self.checks = []
324+ # Iff in an nrpe-external-master relation hook set primary status
325+ relation = relation_ids()
326+ if relation:
327+ if relation[0].startswith('nrpe-external-master'):
328+ log("Setting charm primary status {}".format(primary))
329+ relation_set(relation_settings={'primary': self.primary})
330
331 def add_check(self, *args, **kwargs):
332 self.checks.append(Check(*args, **kwargs))
333
334+ def remove_check(self, *args, **kwargs):
335+ if kwargs.get('shortname') is None:
336+ raise ValueError('shortname of check must be specified')
337+
338+ # Use sensible defaults if they're not specified - these are not
339+ # actually used during removal, but they're required for constructing
340+ # the Check object; check_disk is chosen because it's part of the
341+ # nagios-plugins-basic package.
342+ if kwargs.get('check_cmd') is None:
343+ kwargs['check_cmd'] = 'check_disk'
344+ if kwargs.get('description') is None:
345+ kwargs['description'] = ''
346+
347+ check = Check(*args, **kwargs)
348+ check.remove(self.hostname)
349+
350 def write(self):
351 try:
352 nagios_uid = pwd.getpwnam('nagios').pw_uid
353@@ -248,7 +297,9 @@
354
355 service('restart', 'nagios-nrpe-server')
356
357- for rid in relation_ids("local-monitors"):
358+ monitor_ids = relation_ids("local-monitors") + \
359+ relation_ids("nrpe-external-master")
360+ for rid in monitor_ids:
361 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
362
363
364@@ -259,7 +310,7 @@
365 :param str relation_name: Name of relation nrpe sub joined to
366 """
367 for rel in relations_of_type(relation_name):
368- if 'nagios_hostname' in rel:
369+ if 'nagios_host_context' in rel:
370 return rel['nagios_host_context']
371
372
373@@ -300,11 +351,13 @@
374 upstart_init = '/etc/init/%s.conf' % svc
375 sysv_init = '/etc/init.d/%s' % svc
376 if os.path.exists(upstart_init):
377- nrpe.add_check(
378- shortname=svc,
379- description='process check {%s}' % unit_name,
380- check_cmd='check_upstart_job %s' % svc
381- )
382+ # Don't add a check for these services from neutron-gateway
383+ if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
384+ nrpe.add_check(
385+ shortname=svc,
386+ description='process check {%s}' % unit_name,
387+ check_cmd='check_upstart_job %s' % svc
388+ )
389 elif os.path.exists(sysv_init):
390 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
391 cron_file = ('*/5 * * * * root '
392@@ -322,3 +375,38 @@
393 check_cmd='check_status_file.py -f '
394 '/var/lib/nagios/service-check-%s.txt' % svc,
395 )
396+
397+
398+def copy_nrpe_checks():
399+ """
400+ Copy the nrpe checks into place
401+
402+ """
403+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
404+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
405+ 'charmhelpers', 'contrib', 'openstack',
406+ 'files')
407+
408+ if not os.path.exists(NAGIOS_PLUGINS):
409+ os.makedirs(NAGIOS_PLUGINS)
410+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
411+ if os.path.isfile(fname):
412+ shutil.copy2(fname,
413+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
414+
415+
416+def add_haproxy_checks(nrpe, unit_name):
417+ """
418+ Add checks for each service in list
419+
420+ :param NRPE nrpe: NRPE object to add check to
421+ :param str unit_name: Unit name to use in check description
422+ """
423+ nrpe.add_check(
424+ shortname='haproxy_servers',
425+ description='Check HAProxy {%s}' % unit_name,
426+ check_cmd='check_haproxy.sh')
427+ nrpe.add_check(
428+ shortname='haproxy_queue',
429+ description='Check HAProxy queue depth {%s}' % unit_name,
430+ check_cmd='check_haproxy_queue_depth.sh')
431
432=== added file 'hooks/charmhelpers/core/files.py'
433--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
434+++ hooks/charmhelpers/core/files.py 2016-04-05 14:45:53 +0000
435@@ -0,0 +1,45 @@
436+#!/usr/bin/env python
437+# -*- coding: utf-8 -*-
438+
439+# Copyright 2014-2015 Canonical Limited.
440+#
441+# This file is part of charm-helpers.
442+#
443+# charm-helpers is free software: you can redistribute it and/or modify
444+# it under the terms of the GNU Lesser General Public License version 3 as
445+# published by the Free Software Foundation.
446+#
447+# charm-helpers is distributed in the hope that it will be useful,
448+# but WITHOUT ANY WARRANTY; without even the implied warranty of
449+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
450+# GNU Lesser General Public License for more details.
451+#
452+# You should have received a copy of the GNU Lesser General Public License
453+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
454+
455+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
456+
457+import os
458+import subprocess
459+
460+
461+def sed(filename, before, after, flags='g'):
462+ """
463+ Search and replaces the given pattern on filename.
464+
465+ :param filename: relative or absolute file path.
466+ :param before: expression to be replaced (see 'man sed')
467+ :param after: expression to replace with (see 'man sed')
468+ :param flags: sed-compatible regex flags in example, to make
469+ the search and replace case insensitive, specify ``flags="i"``.
470+ The ``g`` flag is always specified regardless, so you do not
471+ need to remember to include it when overriding this parameter.
472+ :returns: If the sed command exit code was zero then return,
473+ otherwise raise CalledProcessError.
474+ """
475+ expression = r's/{0}/{1}/{2}'.format(before,
476+ after, flags)
477+
478+ return subprocess.check_call(["sed", "-i", "-r", "-e",
479+ expression,
480+ os.path.expanduser(filename)])
481
482=== modified file 'hooks/charmhelpers/core/fstab.py'
483--- hooks/charmhelpers/core/fstab.py 2015-01-27 14:54:02 +0000
484+++ hooks/charmhelpers/core/fstab.py 2016-04-05 14:45:53 +0000
485@@ -17,11 +17,11 @@
486 # You should have received a copy of the GNU Lesser General Public License
487 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
488
489-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
490-
491 import io
492 import os
493
494+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
495+
496
497 class Fstab(io.FileIO):
498 """This class extends file in order to implement a file reader/writer
499@@ -77,7 +77,7 @@
500 for line in self.readlines():
501 line = line.decode('us-ascii')
502 try:
503- if line.strip() and not line.startswith("#"):
504+ if line.strip() and not line.strip().startswith("#"):
505 yield self._hydrate_entry(line)
506 except ValueError:
507 pass
508@@ -104,7 +104,7 @@
509
510 found = False
511 for index, line in enumerate(lines):
512- if not line.startswith("#"):
513+ if line.strip() and not line.strip().startswith("#"):
514 if self._hydrate_entry(line) == entry:
515 found = True
516 break
517
518=== modified file 'hooks/charmhelpers/core/hookenv.py'
519--- hooks/charmhelpers/core/hookenv.py 2015-01-27 14:54:02 +0000
520+++ hooks/charmhelpers/core/hookenv.py 2016-04-05 14:45:53 +0000
521@@ -20,11 +20,18 @@
522 # Authors:
523 # Charm Helpers Developers <juju@lists.ubuntu.com>
524
525+from __future__ import print_function
526+import copy
527+from distutils.version import LooseVersion
528+from functools import wraps
529+import glob
530 import os
531 import json
532 import yaml
533 import subprocess
534 import sys
535+import errno
536+import tempfile
537 from subprocess import CalledProcessError
538
539 import six
540@@ -56,15 +63,18 @@
541
542 will cache the result of unit_get + 'test' for future calls.
543 """
544+ @wraps(func)
545 def wrapper(*args, **kwargs):
546 global cache
547 key = str((func, args, kwargs))
548 try:
549 return cache[key]
550 except KeyError:
551- res = func(*args, **kwargs)
552- cache[key] = res
553- return res
554+ pass # Drop out of the exception handler scope.
555+ res = func(*args, **kwargs)
556+ cache[key] = res
557+ return res
558+ wrapper._wrapped = func
559 return wrapper
560
561
562@@ -87,7 +97,18 @@
563 if not isinstance(message, six.string_types):
564 message = repr(message)
565 command += [message]
566- subprocess.call(command)
567+ # Missing juju-log should not cause failures in unit tests
568+ # Send log output to stderr
569+ try:
570+ subprocess.call(command)
571+ except OSError as e:
572+ if e.errno == errno.ENOENT:
573+ if level:
574+ message = "{}: {}".format(level, message)
575+ message = "juju-log: {}".format(message)
576+ print(message, file=sys.stderr)
577+ else:
578+ raise
579
580
581 class Serializable(UserDict):
582@@ -153,9 +174,19 @@
583 return os.environ.get('JUJU_RELATION', None)
584
585
586-def relation_id():
587- """The relation ID for the current relation hook"""
588- return os.environ.get('JUJU_RELATION_ID', None)
589+@cached
590+def relation_id(relation_name=None, service_or_unit=None):
591+ """The relation ID for the current or a specified relation"""
592+ if not relation_name and not service_or_unit:
593+ return os.environ.get('JUJU_RELATION_ID', None)
594+ elif relation_name and service_or_unit:
595+ service_name = service_or_unit.split('/')[0]
596+ for relid in relation_ids(relation_name):
597+ remote_service = remote_service_name(relid)
598+ if remote_service == service_name:
599+ return relid
600+ else:
601+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
602
603
604 def local_unit():
605@@ -165,7 +196,7 @@
606
607 def remote_unit():
608 """The remote unit for the current relation hook"""
609- return os.environ['JUJU_REMOTE_UNIT']
610+ return os.environ.get('JUJU_REMOTE_UNIT', None)
611
612
613 def service_name():
614@@ -173,9 +204,20 @@
615 return local_unit().split('/')[0]
616
617
618+@cached
619+def remote_service_name(relid=None):
620+ """The remote service name for a given relation-id (or the current relation)"""
621+ if relid is None:
622+ unit = remote_unit()
623+ else:
624+ units = related_units(relid)
625+ unit = units[0] if units else None
626+ return unit.split('/')[0] if unit else None
627+
628+
629 def hook_name():
630 """The name of the currently executing hook"""
631- return os.path.basename(sys.argv[0])
632+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
633
634
635 class Config(dict):
636@@ -225,23 +267,7 @@
637 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
638 if os.path.exists(self.path):
639 self.load_previous()
640-
641- def __getitem__(self, key):
642- """For regular dict lookups, check the current juju config first,
643- then the previous (saved) copy. This ensures that user-saved values
644- will be returned by a dict lookup.
645-
646- """
647- try:
648- return dict.__getitem__(self, key)
649- except KeyError:
650- return (self._prev_dict or {})[key]
651-
652- def keys(self):
653- prev_keys = []
654- if self._prev_dict is not None:
655- prev_keys = self._prev_dict.keys()
656- return list(set(prev_keys + list(dict.keys(self))))
657+ atexit(self._implicit_save)
658
659 def load_previous(self, path=None):
660 """Load previous copy of config from disk.
661@@ -260,6 +286,9 @@
662 self.path = path or self.path
663 with open(self.path) as f:
664 self._prev_dict = json.load(f)
665+ for k, v in copy.deepcopy(self._prev_dict).items():
666+ if k not in self:
667+ self[k] = v
668
669 def changed(self, key):
670 """Return True if the current value for this key is different from
671@@ -291,13 +320,13 @@
672 instance.
673
674 """
675- if self._prev_dict:
676- for k, v in six.iteritems(self._prev_dict):
677- if k not in self:
678- self[k] = v
679 with open(self.path, 'w') as f:
680 json.dump(self, f)
681
682+ def _implicit_save(self):
683+ if self.implicit_save:
684+ self.save()
685+
686
687 @cached
688 def config(scope=None):
689@@ -340,18 +369,49 @@
690 """Set relation information for the current unit"""
691 relation_settings = relation_settings if relation_settings else {}
692 relation_cmd_line = ['relation-set']
693+ accepts_file = "--file" in subprocess.check_output(
694+ relation_cmd_line + ["--help"], universal_newlines=True)
695 if relation_id is not None:
696 relation_cmd_line.extend(('-r', relation_id))
697- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
698- if v is None:
699- relation_cmd_line.append('{}='.format(k))
700- else:
701- relation_cmd_line.append('{}={}'.format(k, v))
702- subprocess.check_call(relation_cmd_line)
703+ settings = relation_settings.copy()
704+ settings.update(kwargs)
705+ for key, value in settings.items():
706+ # Force value to be a string: it always should, but some call
707+ # sites pass in things like dicts or numbers.
708+ if value is not None:
709+ settings[key] = "{}".format(value)
710+ if accepts_file:
711+ # --file was introduced in Juju 1.23.2. Use it by default if
712+ # available, since otherwise we'll break if the relation data is
713+ # too big. Ideally we should tell relation-set to read the data from
714+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
715+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
716+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
717+ subprocess.check_call(
718+ relation_cmd_line + ["--file", settings_file.name])
719+ os.remove(settings_file.name)
720+ else:
721+ for key, value in settings.items():
722+ if value is None:
723+ relation_cmd_line.append('{}='.format(key))
724+ else:
725+ relation_cmd_line.append('{}={}'.format(key, value))
726+ subprocess.check_call(relation_cmd_line)
727 # Flush cache of any relation-gets for local unit
728 flush(local_unit())
729
730
731+def relation_clear(r_id=None):
732+ ''' Clears any relation data already set on relation r_id '''
733+ settings = relation_get(rid=r_id,
734+ unit=local_unit())
735+ for setting in settings:
736+ if setting not in ['public-address', 'private-address']:
737+ settings[setting] = None
738+ relation_set(relation_id=r_id,
739+ **settings)
740+
741+
742 @cached
743 def relation_ids(reltype=None):
744 """A list of relation_ids"""
745@@ -431,6 +491,76 @@
746
747
748 @cached
749+def peer_relation_id():
750+ '''Get the peers relation id if a peers relation has been joined, else None.'''
751+ md = metadata()
752+ section = md.get('peers')
753+ if section:
754+ for key in section:
755+ relids = relation_ids(key)
756+ if relids:
757+ return relids[0]
758+ return None
759+
760+
761+@cached
762+def relation_to_interface(relation_name):
763+ """
764+ Given the name of a relation, return the interface that relation uses.
765+
766+ :returns: The interface name, or ``None``.
767+ """
768+ return relation_to_role_and_interface(relation_name)[1]
769+
770+
771+@cached
772+def relation_to_role_and_interface(relation_name):
773+ """
774+ Given the name of a relation, return the role and the name of the interface
775+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
776+
777+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
778+ """
779+ _metadata = metadata()
780+ for role in ('provides', 'requires', 'peers'):
781+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
782+ if interface:
783+ return role, interface
784+ return None, None
785+
786+
787+@cached
788+def role_and_interface_to_relations(role, interface_name):
789+ """
790+ Given a role and interface name, return a list of relation names for the
791+ current charm that use that interface under that role (where role is one
792+ of ``provides``, ``requires``, or ``peers``).
793+
794+ :returns: A list of relation names.
795+ """
796+ _metadata = metadata()
797+ results = []
798+ for relation_name, relation in _metadata.get(role, {}).items():
799+ if relation['interface'] == interface_name:
800+ results.append(relation_name)
801+ return results
802+
803+
804+@cached
805+def interface_to_relations(interface_name):
806+ """
807+ Given an interface, return a list of relation names for the current
808+ charm that use that interface.
809+
810+ :returns: A list of relation names.
811+ """
812+ results = []
813+ for role in ('provides', 'requires', 'peers'):
814+ results.extend(role_and_interface_to_relations(role, interface_name))
815+ return results
816+
817+
818+@cached
819 def charm_name():
820 """Get the name of the current charm as is specified on metadata.yaml"""
821 return metadata().get('name')
822@@ -496,11 +626,48 @@
823 return None
824
825
826+def unit_public_ip():
827+ """Get this unit's public IP address"""
828+ return unit_get('public-address')
829+
830+
831 def unit_private_ip():
832 """Get this unit's private IP address"""
833 return unit_get('private-address')
834
835
836+@cached
837+def storage_get(attribute=None, storage_id=None):
838+ """Get storage attributes"""
839+ _args = ['storage-get', '--format=json']
840+ if storage_id:
841+ _args.extend(('-s', storage_id))
842+ if attribute:
843+ _args.append(attribute)
844+ try:
845+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
846+ except ValueError:
847+ return None
848+
849+
850+@cached
851+def storage_list(storage_name=None):
852+ """List the storage IDs for the unit"""
853+ _args = ['storage-list', '--format=json']
854+ if storage_name:
855+ _args.append(storage_name)
856+ try:
857+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
858+ except ValueError:
859+ return None
860+ except OSError as e:
861+ import errno
862+ if e.errno == errno.ENOENT:
863+ # storage-list does not exist
864+ return []
865+ raise
866+
867+
868 class UnregisteredHookError(Exception):
869 """Raised when an undefined hook is called"""
870 pass
871@@ -528,10 +695,14 @@
872 hooks.execute(sys.argv)
873 """
874
875- def __init__(self, config_save=True):
876+ def __init__(self, config_save=None):
877 super(Hooks, self).__init__()
878 self._hooks = {}
879- self._config_save = config_save
880+
881+ # For unknown reasons, we allow the Hooks constructor to override
882+ # config().implicit_save.
883+ if config_save is not None:
884+ config().implicit_save = config_save
885
886 def register(self, name, function):
887 """Register a hook"""
888@@ -539,13 +710,16 @@
889
890 def execute(self, args):
891 """Execute a registered hook based on args[0]"""
892+ _run_atstart()
893 hook_name = os.path.basename(args[0])
894 if hook_name in self._hooks:
895- self._hooks[hook_name]()
896- if self._config_save:
897- cfg = config()
898- if cfg.implicit_save:
899- cfg.save()
900+ try:
901+ self._hooks[hook_name]()
902+ except SystemExit as x:
903+ if x.code is None or x.code == 0:
904+ _run_atexit()
905+ raise
906+ _run_atexit()
907 else:
908 raise UnregisteredHookError(hook_name)
909
910@@ -566,3 +740,270 @@
911 def charm_dir():
912 """Return the root directory of the current charm"""
913 return os.environ.get('CHARM_DIR')
914+
915+
916+@cached
917+def action_get(key=None):
918+ """Gets the value of an action parameter, or all key/value param pairs"""
919+ cmd = ['action-get']
920+ if key is not None:
921+ cmd.append(key)
922+ cmd.append('--format=json')
923+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
924+ return action_data
925+
926+
927+def action_set(values):
928+ """Sets the values to be returned after the action finishes"""
929+ cmd = ['action-set']
930+ for k, v in list(values.items()):
931+ cmd.append('{}={}'.format(k, v))
932+ subprocess.check_call(cmd)
933+
934+
935+def action_fail(message):
936+ """Sets the action status to failed and sets the error message.
937+
938+ The results set by action_set are preserved."""
939+ subprocess.check_call(['action-fail', message])
940+
941+
942+def action_name():
943+ """Get the name of the currently executing action."""
944+ return os.environ.get('JUJU_ACTION_NAME')
945+
946+
947+def action_uuid():
948+ """Get the UUID of the currently executing action."""
949+ return os.environ.get('JUJU_ACTION_UUID')
950+
951+
952+def action_tag():
953+ """Get the tag for the currently executing action."""
954+ return os.environ.get('JUJU_ACTION_TAG')
955+
956+
957+def status_set(workload_state, message):
958+ """Set the workload state with a message
959+
960+ Use status-set to set the workload state with a message which is visible
961+ to the user via juju status. If the status-set command is not found then
962+ assume this is juju < 1.23 and juju-log the message unstead.
963+
964+ workload_state -- valid juju workload state.
965+ message -- status update message
966+ """
967+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
968+ if workload_state not in valid_states:
969+ raise ValueError(
970+ '{!r} is not a valid workload state'.format(workload_state)
971+ )
972+ cmd = ['status-set', workload_state, message]
973+ try:
974+ ret = subprocess.call(cmd)
975+ if ret == 0:
976+ return
977+ except OSError as e:
978+ if e.errno != errno.ENOENT:
979+ raise
980+ log_message = 'status-set failed: {} {}'.format(workload_state,
981+ message)
982+ log(log_message, level='INFO')
983+
984+
985+def status_get():
986+ """Retrieve the previously set juju workload state and message
987+
988+ If the status-get command is not found then assume this is juju < 1.23 and
989+ return 'unknown', ""
990+
991+ """
992+ cmd = ['status-get', "--format=json", "--include-data"]
993+ try:
994+ raw_status = subprocess.check_output(cmd)
995+ except OSError as e:
996+ if e.errno == errno.ENOENT:
997+ return ('unknown', "")
998+ else:
999+ raise
1000+ else:
1001+ status = json.loads(raw_status.decode("UTF-8"))
1002+ return (status["status"], status["message"])
1003+
1004+
1005+def translate_exc(from_exc, to_exc):
1006+ def inner_translate_exc1(f):
1007+ @wraps(f)
1008+ def inner_translate_exc2(*args, **kwargs):
1009+ try:
1010+ return f(*args, **kwargs)
1011+ except from_exc:
1012+ raise to_exc
1013+
1014+ return inner_translate_exc2
1015+
1016+ return inner_translate_exc1
1017+
1018+
1019+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1020+def is_leader():
1021+ """Does the current unit hold the juju leadership
1022+
1023+ Uses juju to determine whether the current unit is the leader of its peers
1024+ """
1025+ cmd = ['is-leader', '--format=json']
1026+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1027+
1028+
1029+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1030+def leader_get(attribute=None):
1031+ """Juju leader get value(s)"""
1032+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
1033+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1034+
1035+
1036+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1037+def leader_set(settings=None, **kwargs):
1038+ """Juju leader set value(s)"""
1039+ # Don't log secrets.
1040+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
1041+ cmd = ['leader-set']
1042+ settings = settings or {}
1043+ settings.update(kwargs)
1044+ for k, v in settings.items():
1045+ if v is None:
1046+ cmd.append('{}='.format(k))
1047+ else:
1048+ cmd.append('{}={}'.format(k, v))
1049+ subprocess.check_call(cmd)
1050+
1051+
1052+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1053+def payload_register(ptype, klass, pid):
1054+ """ is used while a hook is running to let Juju know that a
1055+ payload has been started."""
1056+ cmd = ['payload-register']
1057+ for x in [ptype, klass, pid]:
1058+ cmd.append(x)
1059+ subprocess.check_call(cmd)
1060+
1061+
1062+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1063+def payload_unregister(klass, pid):
1064+ """ is used while a hook is running to let Juju know
1065+ that a payload has been manually stopped. The <class> and <id> provided
1066+ must match a payload that has been previously registered with juju using
1067+ payload-register."""
1068+ cmd = ['payload-unregister']
1069+ for x in [klass, pid]:
1070+ cmd.append(x)
1071+ subprocess.check_call(cmd)
1072+
1073+
1074+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1075+def payload_status_set(klass, pid, status):
1076+ """is used to update the current status of a registered payload.
1077+ The <class> and <id> provided must match a payload that has been previously
1078+ registered with juju using payload-register. The <status> must be one of the
1079+ follow: starting, started, stopping, stopped"""
1080+ cmd = ['payload-status-set']
1081+ for x in [klass, pid, status]:
1082+ cmd.append(x)
1083+ subprocess.check_call(cmd)
1084+
1085+
1086+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1087+def resource_get(name):
1088+ """used to fetch the resource path of the given name.
1089+
1090+ <name> must match a name of defined resource in metadata.yaml
1091+
1092+ returns either a path or False if resource not available
1093+ """
1094+ if not name:
1095+ return False
1096+
1097+ cmd = ['resource-get', name]
1098+ try:
1099+ return subprocess.check_output(cmd).decode('UTF-8')
1100+ except subprocess.CalledProcessError:
1101+ return False
1102+
1103+
1104+@cached
1105+def juju_version():
1106+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
1107+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
1108+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
1109+ return subprocess.check_output([jujud, 'version'],
1110+ universal_newlines=True).strip()
1111+
1112+
1113+@cached
1114+def has_juju_version(minimum_version):
1115+ """Return True if the Juju version is at least the provided version"""
1116+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
1117+
1118+
1119+_atexit = []
1120+_atstart = []
1121+
1122+
1123+def atstart(callback, *args, **kwargs):
1124+ '''Schedule a callback to run before the main hook.
1125+
1126+ Callbacks are run in the order they were added.
1127+
1128+ This is useful for modules and classes to perform initialization
1129+ and inject behavior. In particular:
1130+
1131+ - Run common code before all of your hooks, such as logging
1132+ the hook name or interesting relation data.
1133+ - Defer object or module initialization that requires a hook
1134+ context until we know there actually is a hook context,
1135+ making testing easier.
1136+ - Rather than requiring charm authors to include boilerplate to
1137+ invoke your helper's behavior, have it run automatically if
1138+ your object is instantiated or module imported.
1139+
1140+ This is not at all useful after your hook framework as been launched.
1141+ '''
1142+ global _atstart
1143+ _atstart.append((callback, args, kwargs))
1144+
1145+
1146+def atexit(callback, *args, **kwargs):
1147+ '''Schedule a callback to run on successful hook completion.
1148+
1149+ Callbacks are run in the reverse order that they were added.'''
1150+ _atexit.append((callback, args, kwargs))
1151+
1152+
1153+def _run_atstart():
1154+ '''Hook frameworks must invoke this before running the main hook body.'''
1155+ global _atstart
1156+ for callback, args, kwargs in _atstart:
1157+ callback(*args, **kwargs)
1158+ del _atstart[:]
1159+
1160+
1161+def _run_atexit():
1162+ '''Hook frameworks must invoke this after the main hook body has
1163+ successfully completed. Do not invoke it if the hook fails.'''
1164+ global _atexit
1165+ for callback, args, kwargs in reversed(_atexit):
1166+ callback(*args, **kwargs)
1167+ del _atexit[:]
1168+
1169+
1170+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1171+def network_get_primary_address(binding):
1172+ '''
1173+ Retrieve the primary network address for a named binding
1174+
1175+ :param binding: string. The name of a relation of extra-binding
1176+ :return: string. The primary IP address for the named binding
1177+ :raise: NotImplementedError if run on Juju < 2.0
1178+ '''
1179+ cmd = ['network-get', '--primary-address', binding]
1180+ return subprocess.check_output(cmd).strip()
1181
1182=== modified file 'hooks/charmhelpers/core/host.py'
1183--- hooks/charmhelpers/core/host.py 2015-01-27 14:54:02 +0000
1184+++ hooks/charmhelpers/core/host.py 2016-04-05 14:45:53 +0000
1185@@ -24,11 +24,14 @@
1186 import os
1187 import re
1188 import pwd
1189+import glob
1190 import grp
1191 import random
1192 import string
1193 import subprocess
1194 import hashlib
1195+import functools
1196+import itertools
1197 from contextlib import contextmanager
1198 from collections import OrderedDict
1199
1200@@ -62,25 +65,86 @@
1201 return service_result
1202
1203
1204+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
1205+ """Pause a system service.
1206+
1207+ Stop it, and prevent it from starting again at boot."""
1208+ stopped = True
1209+ if service_running(service_name):
1210+ stopped = service_stop(service_name)
1211+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1212+ sysv_file = os.path.join(initd_dir, service_name)
1213+ if init_is_systemd():
1214+ service('disable', service_name)
1215+ elif os.path.exists(upstart_file):
1216+ override_path = os.path.join(
1217+ init_dir, '{}.override'.format(service_name))
1218+ with open(override_path, 'w') as fh:
1219+ fh.write("manual\n")
1220+ elif os.path.exists(sysv_file):
1221+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1222+ else:
1223+ raise ValueError(
1224+ "Unable to detect {0} as SystemD, Upstart {1} or"
1225+ " SysV {2}".format(
1226+ service_name, upstart_file, sysv_file))
1227+ return stopped
1228+
1229+
1230+def service_resume(service_name, init_dir="/etc/init",
1231+ initd_dir="/etc/init.d"):
1232+ """Resume a system service.
1233+
1234+ Reenable starting again at boot. Start the service"""
1235+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1236+ sysv_file = os.path.join(initd_dir, service_name)
1237+ if init_is_systemd():
1238+ service('enable', service_name)
1239+ elif os.path.exists(upstart_file):
1240+ override_path = os.path.join(
1241+ init_dir, '{}.override'.format(service_name))
1242+ if os.path.exists(override_path):
1243+ os.unlink(override_path)
1244+ elif os.path.exists(sysv_file):
1245+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1246+ else:
1247+ raise ValueError(
1248+ "Unable to detect {0} as SystemD, Upstart {1} or"
1249+ " SysV {2}".format(
1250+ service_name, upstart_file, sysv_file))
1251+
1252+ started = service_running(service_name)
1253+ if not started:
1254+ started = service_start(service_name)
1255+ return started
1256+
1257+
1258 def service(action, service_name):
1259 """Control a system service"""
1260- cmd = ['service', service_name, action]
1261+ if init_is_systemd():
1262+ cmd = ['systemctl', action, service_name]
1263+ else:
1264+ cmd = ['service', service_name, action]
1265 return subprocess.call(cmd) == 0
1266
1267
1268-def service_running(service):
1269+def service_running(service_name):
1270 """Determine whether a system service is running"""
1271- try:
1272- output = subprocess.check_output(
1273- ['service', service, 'status'],
1274- stderr=subprocess.STDOUT).decode('UTF-8')
1275- except subprocess.CalledProcessError:
1276- return False
1277+ if init_is_systemd():
1278+ return service('is-active', service_name)
1279 else:
1280- if ("start/running" in output or "is running" in output):
1281- return True
1282- else:
1283+ try:
1284+ output = subprocess.check_output(
1285+ ['service', service_name, 'status'],
1286+ stderr=subprocess.STDOUT).decode('UTF-8')
1287+ except subprocess.CalledProcessError:
1288 return False
1289+ else:
1290+ if ("start/running" in output or "is running" in output or
1291+ "up and running" in output):
1292+ return True
1293+ else:
1294+ return False
1295
1296
1297 def service_available(service_name):
1298@@ -90,13 +154,34 @@
1299 ['service', service_name, 'status'],
1300 stderr=subprocess.STDOUT).decode('UTF-8')
1301 except subprocess.CalledProcessError as e:
1302- return 'unrecognized service' not in e.output
1303+ return b'unrecognized service' not in e.output
1304 else:
1305 return True
1306
1307
1308-def adduser(username, password=None, shell='/bin/bash', system_user=False):
1309- """Add a user to the system"""
1310+SYSTEMD_SYSTEM = '/run/systemd/system'
1311+
1312+
1313+def init_is_systemd():
1314+ """Return True if the host system uses systemd, False otherwise."""
1315+ return os.path.isdir(SYSTEMD_SYSTEM)
1316+
1317+
1318+def adduser(username, password=None, shell='/bin/bash', system_user=False,
1319+ primary_group=None, secondary_groups=None):
1320+ """Add a user to the system.
1321+
1322+ Will log but otherwise succeed if the user already exists.
1323+
1324+ :param str username: Username to create
1325+ :param str password: Password for user; if ``None``, create a system user
1326+ :param str shell: The default shell for the user
1327+ :param bool system_user: Whether to create a login or system user
1328+ :param str primary_group: Primary group for user; defaults to username
1329+ :param list secondary_groups: Optional list of additional groups
1330+
1331+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
1332+ """
1333 try:
1334 user_info = pwd.getpwnam(username)
1335 log('user {0} already exists!'.format(username))
1336@@ -111,12 +196,32 @@
1337 '--shell', shell,
1338 '--password', password,
1339 ])
1340+ if not primary_group:
1341+ try:
1342+ grp.getgrnam(username)
1343+ primary_group = username # avoid "group exists" error
1344+ except KeyError:
1345+ pass
1346+ if primary_group:
1347+ cmd.extend(['-g', primary_group])
1348+ if secondary_groups:
1349+ cmd.extend(['-G', ','.join(secondary_groups)])
1350 cmd.append(username)
1351 subprocess.check_call(cmd)
1352 user_info = pwd.getpwnam(username)
1353 return user_info
1354
1355
1356+def user_exists(username):
1357+ """Check if a user exists"""
1358+ try:
1359+ pwd.getpwnam(username)
1360+ user_exists = True
1361+ except KeyError:
1362+ user_exists = False
1363+ return user_exists
1364+
1365+
1366 def add_group(group_name, system_group=False):
1367 """Add a group to the system"""
1368 try:
1369@@ -139,11 +244,7 @@
1370
1371 def add_user_to_group(username, group):
1372 """Add a user to a group"""
1373- cmd = [
1374- 'gpasswd', '-a',
1375- username,
1376- group
1377- ]
1378+ cmd = ['gpasswd', '-a', username, group]
1379 log("Adding user {} to group {}".format(username, group))
1380 subprocess.check_call(cmd)
1381
1382@@ -191,25 +292,23 @@
1383
1384
1385 def write_file(path, content, owner='root', group='root', perms=0o444):
1386- """Create or overwrite a file with the contents of a string"""
1387+ """Create or overwrite a file with the contents of a byte string."""
1388 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1389 uid = pwd.getpwnam(owner).pw_uid
1390 gid = grp.getgrnam(group).gr_gid
1391- with open(path, 'w') as target:
1392+ with open(path, 'wb') as target:
1393 os.fchown(target.fileno(), uid, gid)
1394 os.fchmod(target.fileno(), perms)
1395 target.write(content)
1396
1397
1398 def fstab_remove(mp):
1399- """Remove the given mountpoint entry from /etc/fstab
1400- """
1401+ """Remove the given mountpoint entry from /etc/fstab"""
1402 return Fstab.remove_by_mountpoint(mp)
1403
1404
1405 def fstab_add(dev, mp, fs, options=None):
1406- """Adds the given device entry to the /etc/fstab file
1407- """
1408+ """Adds the given device entry to the /etc/fstab file"""
1409 return Fstab.add(dev, mp, fs, options=options)
1410
1411
1412@@ -253,9 +352,19 @@
1413 return system_mounts
1414
1415
1416+def fstab_mount(mountpoint):
1417+ """Mount filesystem using fstab"""
1418+ cmd_args = ['mount', mountpoint]
1419+ try:
1420+ subprocess.check_output(cmd_args)
1421+ except subprocess.CalledProcessError as e:
1422+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1423+ return False
1424+ return True
1425+
1426+
1427 def file_hash(path, hash_type='md5'):
1428- """
1429- Generate a hash checksum of the contents of 'path' or None if not found.
1430+ """Generate a hash checksum of the contents of 'path' or None if not found.
1431
1432 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
1433 such as md5, sha1, sha256, sha512, etc.
1434@@ -269,9 +378,22 @@
1435 return None
1436
1437
1438+def path_hash(path):
1439+ """Generate a hash checksum of all files matching 'path'. Standard
1440+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
1441+ module for more information.
1442+
1443+ :return: dict: A { filename: hash } dictionary for all matched files.
1444+ Empty if none found.
1445+ """
1446+ return {
1447+ filename: file_hash(filename)
1448+ for filename in glob.iglob(path)
1449+ }
1450+
1451+
1452 def check_hash(path, checksum, hash_type='md5'):
1453- """
1454- Validate a file using a cryptographic checksum.
1455+ """Validate a file using a cryptographic checksum.
1456
1457 :param str checksum: Value of the checksum used to validate the file.
1458 :param str hash_type: Hash algorithm used to generate `checksum`.
1459@@ -286,6 +408,7 @@
1460
1461
1462 class ChecksumError(ValueError):
1463+ """A class derived from Value error to indicate the checksum failed."""
1464 pass
1465
1466
1467@@ -296,36 +419,58 @@
1468
1469 @restart_on_change({
1470 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
1471+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
1472 })
1473- def ceph_client_changed():
1474+ def config_changed():
1475 pass # your code here
1476
1477 In this example, the cinder-api and cinder-volume services
1478 would be restarted if /etc/ceph/ceph.conf is changed by the
1479- ceph_client_changed function.
1480+ ceph_client_changed function. The apache2 service would be
1481+ restarted if any file matching the pattern got changed, created
1482+ or removed. Standard wildcards are supported, see documentation
1483+ for the 'glob' module for more information.
1484+
1485+ @param restart_map: {path_file_name: [service_name, ...]
1486+ @param stopstart: DEFAULT false; whether to stop, start OR restart
1487+ @returns result from decorated function
1488 """
1489 def wrap(f):
1490- def wrapped_f(*args):
1491- checksums = {}
1492- for path in restart_map:
1493- checksums[path] = file_hash(path)
1494- f(*args)
1495- restarts = []
1496- for path in restart_map:
1497- if checksums[path] != file_hash(path):
1498- restarts += restart_map[path]
1499- services_list = list(OrderedDict.fromkeys(restarts))
1500- if not stopstart:
1501- for service_name in services_list:
1502- service('restart', service_name)
1503- else:
1504- for action in ['stop', 'start']:
1505- for service_name in services_list:
1506- service(action, service_name)
1507+ @functools.wraps(f)
1508+ def wrapped_f(*args, **kwargs):
1509+ return restart_on_change_helper(
1510+ (lambda: f(*args, **kwargs)), restart_map, stopstart)
1511 return wrapped_f
1512 return wrap
1513
1514
1515+def restart_on_change_helper(lambda_f, restart_map, stopstart=False):
1516+ """Helper function to perform the restart_on_change function.
1517+
1518+ This is provided for decorators to restart services if files described
1519+ in the restart_map have changed after an invocation of lambda_f().
1520+
1521+ @param lambda_f: function to call.
1522+ @param restart_map: {file: [service, ...]}
1523+ @param stopstart: whether to stop, start or restart a service
1524+ @returns result of lambda_f()
1525+ """
1526+ checksums = {path: path_hash(path) for path in restart_map}
1527+ r = lambda_f()
1528+ # create a list of lists of the services to restart
1529+ restarts = [restart_map[path]
1530+ for path in restart_map
1531+ if path_hash(path) != checksums[path]]
1532+ # create a flat list of ordered services without duplicates from lists
1533+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
1534+ if services_list:
1535+ actions = ('stop', 'start') if stopstart else ('restart',)
1536+ for action in actions:
1537+ for service_name in services_list:
1538+ service(action, service_name)
1539+ return r
1540+
1541+
1542 def lsb_release():
1543 """Return /etc/lsb-release in a dict"""
1544 d = {}
1545@@ -339,45 +484,105 @@
1546 def pwgen(length=None):
1547 """Generate a random pasword."""
1548 if length is None:
1549+ # A random length is ok to use a weak PRNG
1550 length = random.choice(range(35, 45))
1551 alphanumeric_chars = [
1552 l for l in (string.ascii_letters + string.digits)
1553 if l not in 'l0QD1vAEIOUaeiou']
1554+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
1555+ # actual password
1556+ random_generator = random.SystemRandom()
1557 random_chars = [
1558- random.choice(alphanumeric_chars) for _ in range(length)]
1559+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
1560 return(''.join(random_chars))
1561
1562
1563-def list_nics(nic_type):
1564- '''Return a list of nics of given type(s)'''
1565+def is_phy_iface(interface):
1566+ """Returns True if interface is not virtual, otherwise False."""
1567+ if interface:
1568+ sys_net = '/sys/class/net'
1569+ if os.path.isdir(sys_net):
1570+ for iface in glob.glob(os.path.join(sys_net, '*')):
1571+ if '/virtual/' in os.path.realpath(iface):
1572+ continue
1573+
1574+ if interface == os.path.basename(iface):
1575+ return True
1576+
1577+ return False
1578+
1579+
1580+def get_bond_master(interface):
1581+ """Returns bond master if interface is bond slave otherwise None.
1582+
1583+ NOTE: the provided interface is expected to be physical
1584+ """
1585+ if interface:
1586+ iface_path = '/sys/class/net/%s' % (interface)
1587+ if os.path.exists(iface_path):
1588+ if '/virtual/' in os.path.realpath(iface_path):
1589+ return None
1590+
1591+ master = os.path.join(iface_path, 'master')
1592+ if os.path.exists(master):
1593+ master = os.path.realpath(master)
1594+ # make sure it is a bond master
1595+ if os.path.exists(os.path.join(master, 'bonding')):
1596+ return os.path.basename(master)
1597+
1598+ return None
1599+
1600+
1601+def list_nics(nic_type=None):
1602+ """Return a list of nics of given type(s)"""
1603 if isinstance(nic_type, six.string_types):
1604 int_types = [nic_type]
1605 else:
1606 int_types = nic_type
1607+
1608 interfaces = []
1609- for int_type in int_types:
1610- cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1611+ if nic_type:
1612+ for int_type in int_types:
1613+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1614+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1615+ ip_output = ip_output.split('\n')
1616+ ip_output = (line for line in ip_output if line)
1617+ for line in ip_output:
1618+ if line.split()[1].startswith(int_type):
1619+ matched = re.search('.*: (' + int_type +
1620+ r'[0-9]+\.[0-9]+)@.*', line)
1621+ if matched:
1622+ iface = matched.groups()[0]
1623+ else:
1624+ iface = line.split()[1].replace(":", "")
1625+
1626+ if iface not in interfaces:
1627+ interfaces.append(iface)
1628+ else:
1629+ cmd = ['ip', 'a']
1630 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1631- ip_output = (line for line in ip_output if line)
1632+ ip_output = (line.strip() for line in ip_output if line)
1633+
1634+ key = re.compile('^[0-9]+:\s+(.+):')
1635 for line in ip_output:
1636- if line.split()[1].startswith(int_type):
1637- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
1638- if matched:
1639- interface = matched.groups()[0]
1640- else:
1641- interface = line.split()[1].replace(":", "")
1642- interfaces.append(interface)
1643+ matched = re.search(key, line)
1644+ if matched:
1645+ iface = matched.group(1)
1646+ iface = iface.partition("@")[0]
1647+ if iface not in interfaces:
1648+ interfaces.append(iface)
1649
1650 return interfaces
1651
1652
1653 def set_nic_mtu(nic, mtu):
1654- '''Set MTU on a network interface'''
1655+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
1656 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1657 subprocess.check_call(cmd)
1658
1659
1660 def get_nic_mtu(nic):
1661+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
1662 cmd = ['ip', 'addr', 'show', nic]
1663 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1664 mtu = ""
1665@@ -389,6 +594,7 @@
1666
1667
1668 def get_nic_hwaddr(nic):
1669+ """Return the Media Access Control (MAC) for a network interface."""
1670 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1671 ip_output = subprocess.check_output(cmd).decode('UTF-8')
1672 hwaddr = ""
1673@@ -399,7 +605,7 @@
1674
1675
1676 def cmp_pkgrevno(package, revno, pkgcache=None):
1677- '''Compare supplied revno with the revno of the installed package
1678+ """Compare supplied revno with the revno of the installed package
1679
1680 * 1 => Installed revno is greater than supplied arg
1681 * 0 => Installed revno is the same as supplied arg
1682@@ -408,7 +614,7 @@
1683 This function imports apt_cache function from charmhelpers.fetch if
1684 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1685 you call this function, or pass an apt_pkg.Cache() instance.
1686- '''
1687+ """
1688 import apt_pkg
1689 if not pkgcache:
1690 from charmhelpers.fetch import apt_cache
1691@@ -418,15 +624,30 @@
1692
1693
1694 @contextmanager
1695-def chdir(d):
1696+def chdir(directory):
1697+ """Change the current working directory to a different directory for a code
1698+ block and return the previous directory after the block exits. Useful to
1699+ run commands from a specificed directory.
1700+
1701+ :param str directory: The directory path to change to for this context.
1702+ """
1703 cur = os.getcwd()
1704 try:
1705- yield os.chdir(d)
1706+ yield os.chdir(directory)
1707 finally:
1708 os.chdir(cur)
1709
1710
1711-def chownr(path, owner, group, follow_links=True):
1712+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1713+ """Recursively change user and group ownership of files and directories
1714+ in given path. Doesn't chown path itself by default, only its children.
1715+
1716+ :param str path: The string path to start changing ownership.
1717+ :param str owner: The owner string to use when looking up the uid.
1718+ :param str group: The group string to use when looking up the gid.
1719+ :param bool follow_links: Also Chown links if True
1720+ :param bool chowntopdir: Also chown path itself if True
1721+ """
1722 uid = pwd.getpwnam(owner).pw_uid
1723 gid = grp.getgrnam(group).gr_gid
1724 if follow_links:
1725@@ -434,6 +655,10 @@
1726 else:
1727 chown = os.lchown
1728
1729+ if chowntopdir:
1730+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
1731+ if not broken_symlink:
1732+ chown(path, uid, gid)
1733 for root, dirs, files in os.walk(path):
1734 for name in dirs + files:
1735 full = os.path.join(root, name)
1736@@ -443,4 +668,28 @@
1737
1738
1739 def lchownr(path, owner, group):
1740+ """Recursively change user and group ownership of files and directories
1741+ in a given path, not following symbolic links. See the documentation for
1742+ 'os.lchown' for more information.
1743+
1744+ :param str path: The string path to start changing ownership.
1745+ :param str owner: The owner string to use when looking up the uid.
1746+ :param str group: The group string to use when looking up the gid.
1747+ """
1748 chownr(path, owner, group, follow_links=False)
1749+
1750+
1751+def get_total_ram():
1752+ """The total amount of system RAM in bytes.
1753+
1754+ This is what is reported by the OS, and may be overcommitted when
1755+ there are multiple containers hosted on the same machine.
1756+ """
1757+ with open('/proc/meminfo', 'r') as f:
1758+ for line in f.readlines():
1759+ if line:
1760+ key, value, unit = line.split()
1761+ if key == 'MemTotal:':
1762+ assert unit == 'kB', 'Unknown unit'
1763+ return int(value) * 1024 # Classic, not KiB.
1764+ raise NotImplementedError()
1765
1766=== added file 'hooks/charmhelpers/core/hugepage.py'
1767--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
1768+++ hooks/charmhelpers/core/hugepage.py 2016-04-05 14:45:53 +0000
1769@@ -0,0 +1,71 @@
1770+# -*- coding: utf-8 -*-
1771+
1772+# Copyright 2014-2015 Canonical Limited.
1773+#
1774+# This file is part of charm-helpers.
1775+#
1776+# charm-helpers is free software: you can redistribute it and/or modify
1777+# it under the terms of the GNU Lesser General Public License version 3 as
1778+# published by the Free Software Foundation.
1779+#
1780+# charm-helpers is distributed in the hope that it will be useful,
1781+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1782+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1783+# GNU Lesser General Public License for more details.
1784+#
1785+# You should have received a copy of the GNU Lesser General Public License
1786+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1787+
1788+import yaml
1789+from charmhelpers.core import fstab
1790+from charmhelpers.core import sysctl
1791+from charmhelpers.core.host import (
1792+ add_group,
1793+ add_user_to_group,
1794+ fstab_mount,
1795+ mkdir,
1796+)
1797+from charmhelpers.core.strutils import bytes_from_string
1798+from subprocess import check_output
1799+
1800+
1801+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1802+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
1803+ pagesize='2MB', mount=True, set_shmmax=False):
1804+ """Enable hugepages on system.
1805+
1806+ Args:
1807+ user (str) -- Username to allow access to hugepages to
1808+ group (str) -- Group name to own hugepages
1809+ nr_hugepages (int) -- Number of pages to reserve
1810+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
1811+ mnt_point (str) -- Directory to mount hugepages on
1812+ pagesize (str) -- Size of hugepages
1813+ mount (bool) -- Whether to Mount hugepages
1814+ """
1815+ group_info = add_group(group)
1816+ gid = group_info.gr_gid
1817+ add_user_to_group(user, group)
1818+ if max_map_count < 2 * nr_hugepages:
1819+ max_map_count = 2 * nr_hugepages
1820+ sysctl_settings = {
1821+ 'vm.nr_hugepages': nr_hugepages,
1822+ 'vm.max_map_count': max_map_count,
1823+ 'vm.hugetlb_shm_group': gid,
1824+ }
1825+ if set_shmmax:
1826+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
1827+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
1828+ if shmmax_minsize > shmmax_current:
1829+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
1830+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1831+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1832+ lfstab = fstab.Fstab()
1833+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
1834+ if fstab_entry:
1835+ lfstab.remove_entry(fstab_entry)
1836+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
1837+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
1838+ lfstab.add_entry(entry)
1839+ if mount:
1840+ fstab_mount(mnt_point)
1841
1842=== added file 'hooks/charmhelpers/core/kernel.py'
1843--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
1844+++ hooks/charmhelpers/core/kernel.py 2016-04-05 14:45:53 +0000
1845@@ -0,0 +1,68 @@
1846+#!/usr/bin/env python
1847+# -*- coding: utf-8 -*-
1848+
1849+# Copyright 2014-2015 Canonical Limited.
1850+#
1851+# This file is part of charm-helpers.
1852+#
1853+# charm-helpers is free software: you can redistribute it and/or modify
1854+# it under the terms of the GNU Lesser General Public License version 3 as
1855+# published by the Free Software Foundation.
1856+#
1857+# charm-helpers is distributed in the hope that it will be useful,
1858+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1859+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1860+# GNU Lesser General Public License for more details.
1861+#
1862+# You should have received a copy of the GNU Lesser General Public License
1863+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1864+
1865+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1866+
1867+from charmhelpers.core.hookenv import (
1868+ log,
1869+ INFO
1870+)
1871+
1872+from subprocess import check_call, check_output
1873+import re
1874+
1875+
1876+def modprobe(module, persist=True):
1877+ """Load a kernel module and configure for auto-load on reboot."""
1878+ cmd = ['modprobe', module]
1879+
1880+ log('Loading kernel module %s' % module, level=INFO)
1881+
1882+ check_call(cmd)
1883+ if persist:
1884+ with open('/etc/modules', 'r+') as modules:
1885+ if module not in modules.read():
1886+ modules.write(module)
1887+
1888+
1889+def rmmod(module, force=False):
1890+ """Remove a module from the linux kernel"""
1891+ cmd = ['rmmod']
1892+ if force:
1893+ cmd.append('-f')
1894+ cmd.append(module)
1895+ log('Removing kernel module %s' % module, level=INFO)
1896+ return check_call(cmd)
1897+
1898+
1899+def lsmod():
1900+ """Shows what kernel modules are currently loaded"""
1901+ return check_output(['lsmod'],
1902+ universal_newlines=True)
1903+
1904+
1905+def is_module_loaded(module):
1906+ """Checks if a kernel module is already loaded"""
1907+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1908+ return len(matches) > 0
1909+
1910+
1911+def update_initramfs(version='all'):
1912+ """Updates an initramfs image"""
1913+ return check_call(["update-initramfs", "-k", version, "-u"])
1914
1915=== modified file 'hooks/charmhelpers/core/services/base.py'
1916--- hooks/charmhelpers/core/services/base.py 2015-01-27 14:54:02 +0000
1917+++ hooks/charmhelpers/core/services/base.py 2016-04-05 14:45:53 +0000
1918@@ -15,9 +15,9 @@
1919 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1920
1921 import os
1922-import re
1923 import json
1924-from collections import Iterable
1925+from inspect import getargspec
1926+from collections import Iterable, OrderedDict
1927
1928 from charmhelpers.core import host
1929 from charmhelpers.core import hookenv
1930@@ -119,7 +119,7 @@
1931 """
1932 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
1933 self._ready = None
1934- self.services = {}
1935+ self.services = OrderedDict()
1936 for service in services or []:
1937 service_name = service['service']
1938 self.services[service_name] = service
1939@@ -128,15 +128,18 @@
1940 """
1941 Handle the current hook by doing The Right Thing with the registered services.
1942 """
1943- hook_name = hookenv.hook_name()
1944- if hook_name == 'stop':
1945- self.stop_services()
1946- else:
1947- self.provide_data()
1948- self.reconfigure_services()
1949- cfg = hookenv.config()
1950- if cfg.implicit_save:
1951- cfg.save()
1952+ hookenv._run_atstart()
1953+ try:
1954+ hook_name = hookenv.hook_name()
1955+ if hook_name == 'stop':
1956+ self.stop_services()
1957+ else:
1958+ self.reconfigure_services()
1959+ self.provide_data()
1960+ except SystemExit as x:
1961+ if x.code is None or x.code == 0:
1962+ hookenv._run_atexit()
1963+ hookenv._run_atexit()
1964
1965 def provide_data(self):
1966 """
1967@@ -145,15 +148,36 @@
1968 A provider must have a `name` attribute, which indicates which relation
1969 to set data on, and a `provide_data()` method, which returns a dict of
1970 data to set.
1971+
1972+ The `provide_data()` method can optionally accept two parameters:
1973+
1974+ * ``remote_service`` The name of the remote service that the data will
1975+ be provided to. The `provide_data()` method will be called once
1976+ for each connected service (not unit). This allows the method to
1977+ tailor its data to the given service.
1978+ * ``service_ready`` Whether or not the service definition had all of
1979+ its requirements met, and thus the ``data_ready`` callbacks run.
1980+
1981+ Note that the ``provided_data`` methods are now called **after** the
1982+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
1983+ a chance to generate any data necessary for the providing to the remote
1984+ services.
1985 """
1986- hook_name = hookenv.hook_name()
1987- for service in self.services.values():
1988+ for service_name, service in self.services.items():
1989+ service_ready = self.is_ready(service_name)
1990 for provider in service.get('provided_data', []):
1991- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
1992- data = provider.provide_data()
1993- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
1994- if _ready:
1995- hookenv.relation_set(None, data)
1996+ for relid in hookenv.relation_ids(provider.name):
1997+ units = hookenv.related_units(relid)
1998+ if not units:
1999+ continue
2000+ remote_service = units[0].split('/')[0]
2001+ argspec = getargspec(provider.provide_data)
2002+ if len(argspec.args) > 2:
2003+ data = provider.provide_data(remote_service, service_ready)
2004+ else:
2005+ data = provider.provide_data()
2006+ if data:
2007+ hookenv.relation_set(relid, data)
2008
2009 def reconfigure_services(self, *service_names):
2010 """
2011
2012=== modified file 'hooks/charmhelpers/core/services/helpers.py'
2013--- hooks/charmhelpers/core/services/helpers.py 2015-01-27 14:54:02 +0000
2014+++ hooks/charmhelpers/core/services/helpers.py 2016-04-05 14:45:53 +0000
2015@@ -16,7 +16,9 @@
2016
2017 import os
2018 import yaml
2019+
2020 from charmhelpers.core import hookenv
2021+from charmhelpers.core import host
2022 from charmhelpers.core import templating
2023
2024 from charmhelpers.core.services.base import ManagerCallback
2025@@ -45,12 +47,14 @@
2026 """
2027 name = None
2028 interface = None
2029- required_keys = []
2030
2031 def __init__(self, name=None, additional_required_keys=None):
2032+ if not hasattr(self, 'required_keys'):
2033+ self.required_keys = []
2034+
2035 if name is not None:
2036 self.name = name
2037- if additional_required_keys is not None:
2038+ if additional_required_keys:
2039 self.required_keys.extend(additional_required_keys)
2040 self.get_data()
2041
2042@@ -134,7 +138,10 @@
2043 """
2044 name = 'db'
2045 interface = 'mysql'
2046- required_keys = ['host', 'user', 'password', 'database']
2047+
2048+ def __init__(self, *args, **kwargs):
2049+ self.required_keys = ['host', 'user', 'password', 'database']
2050+ RelationContext.__init__(self, *args, **kwargs)
2051
2052
2053 class HttpRelation(RelationContext):
2054@@ -146,7 +153,10 @@
2055 """
2056 name = 'website'
2057 interface = 'http'
2058- required_keys = ['host', 'port']
2059+
2060+ def __init__(self, *args, **kwargs):
2061+ self.required_keys = ['host', 'port']
2062+ RelationContext.__init__(self, *args, **kwargs)
2063
2064 def provide_data(self):
2065 return {
2066@@ -231,28 +241,51 @@
2067 action.
2068
2069 :param str source: The template source file, relative to
2070- `$CHARM_DIR/templates`
2071+ `$CHARM_DIR/templates`
2072
2073- :param str target: The target to write the rendered template to
2074+ :param str target: The target to write the rendered template to (or None)
2075 :param str owner: The owner of the rendered file
2076 :param str group: The group of the rendered file
2077 :param int perms: The permissions of the rendered file
2078+ :param partial on_change_action: functools partial to be executed when
2079+ rendered file changes
2080+ :param jinja2 loader template_loader: A jinja2 template loader
2081+
2082+ :return str: The rendered template
2083 """
2084 def __init__(self, source, target,
2085- owner='root', group='root', perms=0o444):
2086+ owner='root', group='root', perms=0o444,
2087+ on_change_action=None, template_loader=None):
2088 self.source = source
2089 self.target = target
2090 self.owner = owner
2091 self.group = group
2092 self.perms = perms
2093+ self.on_change_action = on_change_action
2094+ self.template_loader = template_loader
2095
2096 def __call__(self, manager, service_name, event_name):
2097+ pre_checksum = ''
2098+ if self.on_change_action and os.path.isfile(self.target):
2099+ pre_checksum = host.file_hash(self.target)
2100 service = manager.get_service(service_name)
2101- context = {}
2102+ context = {'ctx': {}}
2103 for ctx in service.get('required_data', []):
2104 context.update(ctx)
2105- templating.render(self.source, self.target, context,
2106- self.owner, self.group, self.perms)
2107+ context['ctx'].update(ctx)
2108+
2109+ result = templating.render(self.source, self.target, context,
2110+ self.owner, self.group, self.perms,
2111+ template_loader=self.template_loader)
2112+ if self.on_change_action:
2113+ if pre_checksum == host.file_hash(self.target):
2114+ hookenv.log(
2115+ 'No change detected: {}'.format(self.target),
2116+ hookenv.DEBUG)
2117+ else:
2118+ self.on_change_action()
2119+
2120+ return result
2121
2122
2123 # Convenience aliases for templates
2124
2125=== added file 'hooks/charmhelpers/core/strutils.py'
2126--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
2127+++ hooks/charmhelpers/core/strutils.py 2016-04-05 14:45:53 +0000
2128@@ -0,0 +1,72 @@
2129+#!/usr/bin/env python
2130+# -*- coding: utf-8 -*-
2131+
2132+# Copyright 2014-2015 Canonical Limited.
2133+#
2134+# This file is part of charm-helpers.
2135+#
2136+# charm-helpers is free software: you can redistribute it and/or modify
2137+# it under the terms of the GNU Lesser General Public License version 3 as
2138+# published by the Free Software Foundation.
2139+#
2140+# charm-helpers is distributed in the hope that it will be useful,
2141+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2142+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2143+# GNU Lesser General Public License for more details.
2144+#
2145+# You should have received a copy of the GNU Lesser General Public License
2146+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2147+
2148+import six
2149+import re
2150+
2151+
2152+def bool_from_string(value):
2153+ """Interpret string value as boolean.
2154+
2155+ Returns True if value translates to True otherwise False.
2156+ """
2157+ if isinstance(value, six.string_types):
2158+ value = six.text_type(value)
2159+ else:
2160+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2161+ raise ValueError(msg)
2162+
2163+ value = value.strip().lower()
2164+
2165+ if value in ['y', 'yes', 'true', 't', 'on']:
2166+ return True
2167+ elif value in ['n', 'no', 'false', 'f', 'off']:
2168+ return False
2169+
2170+ msg = "Unable to interpret string value '%s' as boolean" % (value)
2171+ raise ValueError(msg)
2172+
2173+
2174+def bytes_from_string(value):
2175+ """Interpret human readable string value as bytes.
2176+
2177+ Returns int
2178+ """
2179+ BYTE_POWER = {
2180+ 'K': 1,
2181+ 'KB': 1,
2182+ 'M': 2,
2183+ 'MB': 2,
2184+ 'G': 3,
2185+ 'GB': 3,
2186+ 'T': 4,
2187+ 'TB': 4,
2188+ 'P': 5,
2189+ 'PB': 5,
2190+ }
2191+ if isinstance(value, six.string_types):
2192+ value = six.text_type(value)
2193+ else:
2194+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2195+ raise ValueError(msg)
2196+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
2197+ if not matches:
2198+ msg = "Unable to interpret string value '%s' as bytes" % (value)
2199+ raise ValueError(msg)
2200+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
2201
2202=== modified file 'hooks/charmhelpers/core/sysctl.py'
2203--- hooks/charmhelpers/core/sysctl.py 2015-01-27 14:54:02 +0000
2204+++ hooks/charmhelpers/core/sysctl.py 2016-04-05 14:45:53 +0000
2205@@ -17,8 +17,6 @@
2206 # You should have received a copy of the GNU Lesser General Public License
2207 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2208
2209-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2210-
2211 import yaml
2212
2213 from subprocess import check_call
2214@@ -26,25 +24,33 @@
2215 from charmhelpers.core.hookenv import (
2216 log,
2217 DEBUG,
2218+ ERROR,
2219 )
2220
2221+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2222+
2223
2224 def create(sysctl_dict, sysctl_file):
2225 """Creates a sysctl.conf file from a YAML associative array
2226
2227- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
2228- :type sysctl_dict: dict
2229+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
2230+ :type sysctl_dict: str
2231 :param sysctl_file: path to the sysctl file to be saved
2232 :type sysctl_file: str or unicode
2233 :returns: None
2234 """
2235- sysctl_dict = yaml.load(sysctl_dict)
2236+ try:
2237+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
2238+ except yaml.YAMLError:
2239+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
2240+ level=ERROR)
2241+ return
2242
2243 with open(sysctl_file, "w") as fd:
2244- for key, value in sysctl_dict.items():
2245+ for key, value in sysctl_dict_parsed.items():
2246 fd.write("{}={}\n".format(key, value))
2247
2248- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
2249+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
2250 level=DEBUG)
2251
2252 check_call(["sysctl", "-p", sysctl_file])
2253
2254=== modified file 'hooks/charmhelpers/core/templating.py'
2255--- hooks/charmhelpers/core/templating.py 2015-01-27 14:54:02 +0000
2256+++ hooks/charmhelpers/core/templating.py 2016-04-05 14:45:53 +0000
2257@@ -21,13 +21,14 @@
2258
2259
2260 def render(source, target, context, owner='root', group='root',
2261- perms=0o444, templates_dir=None):
2262+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
2263 """
2264 Render a template.
2265
2266 The `source` path, if not absolute, is relative to the `templates_dir`.
2267
2268- The `target` path should be absolute.
2269+ The `target` path should be absolute. It can also be `None`, in which
2270+ case no file will be written.
2271
2272 The context should be a dict containing the values to be replaced in the
2273 template.
2274@@ -36,6 +37,9 @@
2275
2276 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
2277
2278+ The rendered template will be written to the file as well as being returned
2279+ as a string.
2280+
2281 Note: Using this requires python-jinja2; if it is not installed, calling
2282 this will attempt to use charmhelpers.fetch.apt_install to install it.
2283 """
2284@@ -52,17 +56,26 @@
2285 apt_install('python-jinja2', fatal=True)
2286 from jinja2 import FileSystemLoader, Environment, exceptions
2287
2288- if templates_dir is None:
2289- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2290- loader = Environment(loader=FileSystemLoader(templates_dir))
2291+ if template_loader:
2292+ template_env = Environment(loader=template_loader)
2293+ else:
2294+ if templates_dir is None:
2295+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2296+ template_env = Environment(loader=FileSystemLoader(templates_dir))
2297 try:
2298 source = source
2299- template = loader.get_template(source)
2300+ template = template_env.get_template(source)
2301 except exceptions.TemplateNotFound as e:
2302 hookenv.log('Could not load template %s from %s.' %
2303 (source, templates_dir),
2304 level=hookenv.ERROR)
2305 raise e
2306 content = template.render(context)
2307- host.mkdir(os.path.dirname(target), owner, group)
2308- host.write_file(target, content, owner, group, perms)
2309+ if target is not None:
2310+ target_dir = os.path.dirname(target)
2311+ if not os.path.exists(target_dir):
2312+ # This is a terrible default directory permission, as the file
2313+ # or its siblings will often contain secrets.
2314+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2315+ host.write_file(target, content.encode(encoding), owner, group, perms)
2316+ return content
2317
2318=== added file 'hooks/charmhelpers/core/unitdata.py'
2319--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
2320+++ hooks/charmhelpers/core/unitdata.py 2016-04-05 14:45:53 +0000
2321@@ -0,0 +1,521 @@
2322+#!/usr/bin/env python
2323+# -*- coding: utf-8 -*-
2324+#
2325+# Copyright 2014-2015 Canonical Limited.
2326+#
2327+# This file is part of charm-helpers.
2328+#
2329+# charm-helpers is free software: you can redistribute it and/or modify
2330+# it under the terms of the GNU Lesser General Public License version 3 as
2331+# published by the Free Software Foundation.
2332+#
2333+# charm-helpers is distributed in the hope that it will be useful,
2334+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2335+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2336+# GNU Lesser General Public License for more details.
2337+#
2338+# You should have received a copy of the GNU Lesser General Public License
2339+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2340+#
2341+#
2342+# Authors:
2343+# Kapil Thangavelu <kapil.foss@gmail.com>
2344+#
2345+"""
2346+Intro
2347+-----
2348+
2349+A simple way to store state in units. This provides a key value
2350+storage with support for versioned, transactional operation,
2351+and can calculate deltas from previous values to simplify unit logic
2352+when processing changes.
2353+
2354+
2355+Hook Integration
2356+----------------
2357+
2358+There are several extant frameworks for hook execution, including
2359+
2360+ - charmhelpers.core.hookenv.Hooks
2361+ - charmhelpers.core.services.ServiceManager
2362+
2363+The storage classes are framework agnostic, one simple integration is
2364+via the HookData contextmanager. It will record the current hook
2365+execution environment (including relation data, config data, etc.),
2366+setup a transaction and allow easy access to the changes from
2367+previously seen values. One consequence of the integration is the
2368+reservation of particular keys ('rels', 'unit', 'env', 'config',
2369+'charm_revisions') for their respective values.
2370+
2371+Here's a fully worked integration example using hookenv.Hooks::
2372+
2373+ from charmhelper.core import hookenv, unitdata
2374+
2375+ hook_data = unitdata.HookData()
2376+ db = unitdata.kv()
2377+ hooks = hookenv.Hooks()
2378+
2379+ @hooks.hook
2380+ def config_changed():
2381+ # Print all changes to configuration from previously seen
2382+ # values.
2383+ for changed, (prev, cur) in hook_data.conf.items():
2384+ print('config changed', changed,
2385+ 'previous value', prev,
2386+ 'current value', cur)
2387+
2388+ # Get some unit specific bookeeping
2389+ if not db.get('pkg_key'):
2390+ key = urllib.urlopen('https://example.com/pkg_key').read()
2391+ db.set('pkg_key', key)
2392+
2393+ # Directly access all charm config as a mapping.
2394+ conf = db.getrange('config', True)
2395+
2396+ # Directly access all relation data as a mapping
2397+ rels = db.getrange('rels', True)
2398+
2399+ if __name__ == '__main__':
2400+ with hook_data():
2401+ hook.execute()
2402+
2403+
2404+A more basic integration is via the hook_scope context manager which simply
2405+manages transaction scope (and records hook name, and timestamp)::
2406+
2407+ >>> from unitdata import kv
2408+ >>> db = kv()
2409+ >>> with db.hook_scope('install'):
2410+ ... # do work, in transactional scope.
2411+ ... db.set('x', 1)
2412+ >>> db.get('x')
2413+ 1
2414+
2415+
2416+Usage
2417+-----
2418+
2419+Values are automatically json de/serialized to preserve basic typing
2420+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
2421+
2422+Individual values can be manipulated via get/set::
2423+
2424+ >>> kv.set('y', True)
2425+ >>> kv.get('y')
2426+ True
2427+
2428+ # We can set complex values (dicts, lists) as a single key.
2429+ >>> kv.set('config', {'a': 1, 'b': True'})
2430+
2431+ # Also supports returning dictionaries as a record which
2432+ # provides attribute access.
2433+ >>> config = kv.get('config', record=True)
2434+ >>> config.b
2435+ True
2436+
2437+
2438+Groups of keys can be manipulated with update/getrange::
2439+
2440+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
2441+ >>> kv.getrange('gui.', strip=True)
2442+ {'z': 1, 'y': 2}
2443+
2444+When updating values, its very helpful to understand which values
2445+have actually changed and how have they changed. The storage
2446+provides a delta method to provide for this::
2447+
2448+ >>> data = {'debug': True, 'option': 2}
2449+ >>> delta = kv.delta(data, 'config.')
2450+ >>> delta.debug.previous
2451+ None
2452+ >>> delta.debug.current
2453+ True
2454+ >>> delta
2455+ {'debug': (None, True), 'option': (None, 2)}
2456+
2457+Note the delta method does not persist the actual change, it needs to
2458+be explicitly saved via 'update' method::
2459+
2460+ >>> kv.update(data, 'config.')
2461+
2462+Values modified in the context of a hook scope retain historical values
2463+associated to the hookname.
2464+
2465+ >>> with db.hook_scope('config-changed'):
2466+ ... db.set('x', 42)
2467+ >>> db.gethistory('x')
2468+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
2469+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
2470+
2471+"""
2472+
2473+import collections
2474+import contextlib
2475+import datetime
2476+import itertools
2477+import json
2478+import os
2479+import pprint
2480+import sqlite3
2481+import sys
2482+
2483+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
2484+
2485+
2486+class Storage(object):
2487+ """Simple key value database for local unit state within charms.
2488+
2489+ Modifications are not persisted unless :meth:`flush` is called.
2490+
2491+ To support dicts, lists, integer, floats, and booleans values
2492+ are automatically json encoded/decoded.
2493+ """
2494+ def __init__(self, path=None):
2495+ self.db_path = path
2496+ if path is None:
2497+ if 'UNIT_STATE_DB' in os.environ:
2498+ self.db_path = os.environ['UNIT_STATE_DB']
2499+ else:
2500+ self.db_path = os.path.join(
2501+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
2502+ self.conn = sqlite3.connect('%s' % self.db_path)
2503+ self.cursor = self.conn.cursor()
2504+ self.revision = None
2505+ self._closed = False
2506+ self._init()
2507+
2508+ def close(self):
2509+ if self._closed:
2510+ return
2511+ self.flush(False)
2512+ self.cursor.close()
2513+ self.conn.close()
2514+ self._closed = True
2515+
2516+ def get(self, key, default=None, record=False):
2517+ self.cursor.execute('select data from kv where key=?', [key])
2518+ result = self.cursor.fetchone()
2519+ if not result:
2520+ return default
2521+ if record:
2522+ return Record(json.loads(result[0]))
2523+ return json.loads(result[0])
2524+
2525+ def getrange(self, key_prefix, strip=False):
2526+ """
2527+ Get a range of keys starting with a common prefix as a mapping of
2528+ keys to values.
2529+
2530+ :param str key_prefix: Common prefix among all keys
2531+ :param bool strip: Optionally strip the common prefix from the key
2532+ names in the returned dict
2533+ :return dict: A (possibly empty) dict of key-value mappings
2534+ """
2535+ self.cursor.execute("select key, data from kv where key like ?",
2536+ ['%s%%' % key_prefix])
2537+ result = self.cursor.fetchall()
2538+
2539+ if not result:
2540+ return {}
2541+ if not strip:
2542+ key_prefix = ''
2543+ return dict([
2544+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
2545+
2546+ def update(self, mapping, prefix=""):
2547+ """
2548+ Set the values of multiple keys at once.
2549+
2550+ :param dict mapping: Mapping of keys to values
2551+ :param str prefix: Optional prefix to apply to all keys in `mapping`
2552+ before setting
2553+ """
2554+ for k, v in mapping.items():
2555+ self.set("%s%s" % (prefix, k), v)
2556+
2557+ def unset(self, key):
2558+ """
2559+ Remove a key from the database entirely.
2560+ """
2561+ self.cursor.execute('delete from kv where key=?', [key])
2562+ if self.revision and self.cursor.rowcount:
2563+ self.cursor.execute(
2564+ 'insert into kv_revisions values (?, ?, ?)',
2565+ [key, self.revision, json.dumps('DELETED')])
2566+
2567+ def unsetrange(self, keys=None, prefix=""):
2568+ """
2569+ Remove a range of keys starting with a common prefix, from the database
2570+ entirely.
2571+
2572+ :param list keys: List of keys to remove.
2573+ :param str prefix: Optional prefix to apply to all keys in ``keys``
2574+ before removing.
2575+ """
2576+ if keys is not None:
2577+ keys = ['%s%s' % (prefix, key) for key in keys]
2578+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
2579+ if self.revision and self.cursor.rowcount:
2580+ self.cursor.execute(
2581+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
2582+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
2583+ else:
2584+ self.cursor.execute('delete from kv where key like ?',
2585+ ['%s%%' % prefix])
2586+ if self.revision and self.cursor.rowcount:
2587+ self.cursor.execute(
2588+ 'insert into kv_revisions values (?, ?, ?)',
2589+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
2590+
2591+ def set(self, key, value):
2592+ """
2593+ Set a value in the database.
2594+
2595+ :param str key: Key to set the value for
2596+ :param value: Any JSON-serializable value to be set
2597+ """
2598+ serialized = json.dumps(value)
2599+
2600+ self.cursor.execute('select data from kv where key=?', [key])
2601+ exists = self.cursor.fetchone()
2602+
2603+ # Skip mutations to the same value
2604+ if exists:
2605+ if exists[0] == serialized:
2606+ return value
2607+
2608+ if not exists:
2609+ self.cursor.execute(
2610+ 'insert into kv (key, data) values (?, ?)',
2611+ (key, serialized))
2612+ else:
2613+ self.cursor.execute('''
2614+ update kv
2615+ set data = ?
2616+ where key = ?''', [serialized, key])
2617+
2618+ # Save
2619+ if not self.revision:
2620+ return value
2621+
2622+ self.cursor.execute(
2623+ 'select 1 from kv_revisions where key=? and revision=?',
2624+ [key, self.revision])
2625+ exists = self.cursor.fetchone()
2626+
2627+ if not exists:
2628+ self.cursor.execute(
2629+ '''insert into kv_revisions (
2630+ revision, key, data) values (?, ?, ?)''',
2631+ (self.revision, key, serialized))
2632+ else:
2633+ self.cursor.execute(
2634+ '''
2635+ update kv_revisions
2636+ set data = ?
2637+ where key = ?
2638+ and revision = ?''',
2639+ [serialized, key, self.revision])
2640+
2641+ return value
2642+
2643+ def delta(self, mapping, prefix):
2644+ """
2645+ return a delta containing values that have changed.
2646+ """
2647+ previous = self.getrange(prefix, strip=True)
2648+ if not previous:
2649+ pk = set()
2650+ else:
2651+ pk = set(previous.keys())
2652+ ck = set(mapping.keys())
2653+ delta = DeltaSet()
2654+
2655+ # added
2656+ for k in ck.difference(pk):
2657+ delta[k] = Delta(None, mapping[k])
2658+
2659+ # removed
2660+ for k in pk.difference(ck):
2661+ delta[k] = Delta(previous[k], None)
2662+
2663+ # changed
2664+ for k in pk.intersection(ck):
2665+ c = mapping[k]
2666+ p = previous[k]
2667+ if c != p:
2668+ delta[k] = Delta(p, c)
2669+
2670+ return delta
2671+
2672+ @contextlib.contextmanager
2673+ def hook_scope(self, name=""):
2674+ """Scope all future interactions to the current hook execution
2675+ revision."""
2676+ assert not self.revision
2677+ self.cursor.execute(
2678+ 'insert into hooks (hook, date) values (?, ?)',
2679+ (name or sys.argv[0],
2680+ datetime.datetime.utcnow().isoformat()))
2681+ self.revision = self.cursor.lastrowid
2682+ try:
2683+ yield self.revision
2684+ self.revision = None
2685+ except:
2686+ self.flush(False)
2687+ self.revision = None
2688+ raise
2689+ else:
2690+ self.flush()
2691+
2692+ def flush(self, save=True):
2693+ if save:
2694+ self.conn.commit()
2695+ elif self._closed:
2696+ return
2697+ else:
2698+ self.conn.rollback()
2699+
2700+ def _init(self):
2701+ self.cursor.execute('''
2702+ create table if not exists kv (
2703+ key text,
2704+ data text,
2705+ primary key (key)
2706+ )''')
2707+ self.cursor.execute('''
2708+ create table if not exists kv_revisions (
2709+ key text,
2710+ revision integer,
2711+ data text,
2712+ primary key (key, revision)
2713+ )''')
2714+ self.cursor.execute('''
2715+ create table if not exists hooks (
2716+ version integer primary key autoincrement,
2717+ hook text,
2718+ date text
2719+ )''')
2720+ self.conn.commit()
2721+
2722+ def gethistory(self, key, deserialize=False):
2723+ self.cursor.execute(
2724+ '''
2725+ select kv.revision, kv.key, kv.data, h.hook, h.date
2726+ from kv_revisions kv,
2727+ hooks h
2728+ where kv.key=?
2729+ and kv.revision = h.version
2730+ ''', [key])
2731+ if deserialize is False:
2732+ return self.cursor.fetchall()
2733+ return map(_parse_history, self.cursor.fetchall())
2734+
2735+ def debug(self, fh=sys.stderr):
2736+ self.cursor.execute('select * from kv')
2737+ pprint.pprint(self.cursor.fetchall(), stream=fh)
2738+ self.cursor.execute('select * from kv_revisions')
2739+ pprint.pprint(self.cursor.fetchall(), stream=fh)
2740+
2741+
2742+def _parse_history(d):
2743+ return (d[0], d[1], json.loads(d[2]), d[3],
2744+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
2745+
2746+
2747+class HookData(object):
2748+ """Simple integration for existing hook exec frameworks.
2749+
2750+ Records all unit information, and stores deltas for processing
2751+ by the hook.
2752+
2753+ Sample::
2754+
2755+ from charmhelper.core import hookenv, unitdata
2756+
2757+ changes = unitdata.HookData()
2758+ db = unitdata.kv()
2759+ hooks = hookenv.Hooks()
2760+
2761+ @hooks.hook
2762+ def config_changed():
2763+ # View all changes to configuration
2764+ for changed, (prev, cur) in changes.conf.items():
2765+ print('config changed', changed,
2766+ 'previous value', prev,
2767+ 'current value', cur)
2768+
2769+ # Get some unit specific bookeeping
2770+ if not db.get('pkg_key'):
2771+ key = urllib.urlopen('https://example.com/pkg_key').read()
2772+ db.set('pkg_key', key)
2773+
2774+ if __name__ == '__main__':
2775+ with changes():
2776+ hook.execute()
2777+
2778+ """
2779+ def __init__(self):
2780+ self.kv = kv()
2781+ self.conf = None
2782+ self.rels = None
2783+
2784+ @contextlib.contextmanager
2785+ def __call__(self):
2786+ from charmhelpers.core import hookenv
2787+ hook_name = hookenv.hook_name()
2788+
2789+ with self.kv.hook_scope(hook_name):
2790+ self._record_charm_version(hookenv.charm_dir())
2791+ delta_config, delta_relation = self._record_hook(hookenv)
2792+ yield self.kv, delta_config, delta_relation
2793+
2794+ def _record_charm_version(self, charm_dir):
2795+ # Record revisions.. charm revisions are meaningless
2796+ # to charm authors as they don't control the revision.
2797+ # so logic dependnent on revision is not particularly
2798+ # useful, however it is useful for debugging analysis.
2799+ charm_rev = open(
2800+ os.path.join(charm_dir, 'revision')).read().strip()
2801+ charm_rev = charm_rev or '0'
2802+ revs = self.kv.get('charm_revisions', [])
2803+ if charm_rev not in revs:
2804+ revs.append(charm_rev.strip() or '0')
2805+ self.kv.set('charm_revisions', revs)
2806+
2807+ def _record_hook(self, hookenv):
2808+ data = hookenv.execution_environment()
2809+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
2810+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
2811+ self.kv.set('env', dict(data['env']))
2812+ self.kv.set('unit', data['unit'])
2813+ self.kv.set('relid', data.get('relid'))
2814+ return conf_delta, rels_delta
2815+
2816+
2817+class Record(dict):
2818+
2819+ __slots__ = ()
2820+
2821+ def __getattr__(self, k):
2822+ if k in self:
2823+ return self[k]
2824+ raise AttributeError(k)
2825+
2826+
2827+class DeltaSet(Record):
2828+
2829+ __slots__ = ()
2830+
2831+
2832+Delta = collections.namedtuple('Delta', ['previous', 'current'])
2833+
2834+
2835+_KV = None
2836+
2837+
2838+def kv():
2839+ global _KV
2840+ if _KV is None:
2841+ _KV = Storage()
2842+ return _KV
2843
2844=== modified file 'hooks/charmhelpers/fetch/__init__.py'
2845--- hooks/charmhelpers/fetch/__init__.py 2015-01-27 14:54:02 +0000
2846+++ hooks/charmhelpers/fetch/__init__.py 2016-04-05 14:45:53 +0000
2847@@ -90,6 +90,22 @@
2848 'kilo/proposed': 'trusty-proposed/kilo',
2849 'trusty-kilo/proposed': 'trusty-proposed/kilo',
2850 'trusty-proposed/kilo': 'trusty-proposed/kilo',
2851+ # Liberty
2852+ 'liberty': 'trusty-updates/liberty',
2853+ 'trusty-liberty': 'trusty-updates/liberty',
2854+ 'trusty-liberty/updates': 'trusty-updates/liberty',
2855+ 'trusty-updates/liberty': 'trusty-updates/liberty',
2856+ 'liberty/proposed': 'trusty-proposed/liberty',
2857+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
2858+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
2859+ # Mitaka
2860+ 'mitaka': 'trusty-updates/mitaka',
2861+ 'trusty-mitaka': 'trusty-updates/mitaka',
2862+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
2863+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
2864+ 'mitaka/proposed': 'trusty-proposed/mitaka',
2865+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
2866+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
2867 }
2868
2869 # The order of this list is very important. Handlers should be listed in from
2870@@ -158,7 +174,7 @@
2871
2872 def apt_cache(in_memory=True):
2873 """Build and return an apt cache"""
2874- import apt_pkg
2875+ from apt import apt_pkg
2876 apt_pkg.init()
2877 if in_memory:
2878 apt_pkg.config.set("Dir::Cache::pkgcache", "")
2879@@ -215,19 +231,27 @@
2880 _run_apt_command(cmd, fatal)
2881
2882
2883+def apt_mark(packages, mark, fatal=False):
2884+ """Flag one or more packages using apt-mark"""
2885+ log("Marking {} as {}".format(packages, mark))
2886+ cmd = ['apt-mark', mark]
2887+ if isinstance(packages, six.string_types):
2888+ cmd.append(packages)
2889+ else:
2890+ cmd.extend(packages)
2891+
2892+ if fatal:
2893+ subprocess.check_call(cmd, universal_newlines=True)
2894+ else:
2895+ subprocess.call(cmd, universal_newlines=True)
2896+
2897+
2898 def apt_hold(packages, fatal=False):
2899- """Hold one or more packages"""
2900- cmd = ['apt-mark', 'hold']
2901- if isinstance(packages, six.string_types):
2902- cmd.append(packages)
2903- else:
2904- cmd.extend(packages)
2905- log("Holding {}".format(packages))
2906-
2907- if fatal:
2908- subprocess.check_call(cmd)
2909- else:
2910- subprocess.call(cmd)
2911+ return apt_mark(packages, 'hold', fatal=fatal)
2912+
2913+
2914+def apt_unhold(packages, fatal=False):
2915+ return apt_mark(packages, 'unhold', fatal=fatal)
2916
2917
2918 def add_source(source, key=None):
2919@@ -370,8 +394,9 @@
2920 for handler in handlers:
2921 try:
2922 installed_to = handler.install(source, *args, **kwargs)
2923- except UnhandledSource:
2924- pass
2925+ except UnhandledSource as e:
2926+ log('Install source attempt unsuccessful: {}'.format(e),
2927+ level='WARNING')
2928 if not installed_to:
2929 raise UnhandledSource("No handler found for source {}".format(source))
2930 return installed_to
2931@@ -394,7 +419,7 @@
2932 importlib.import_module(package),
2933 classname)
2934 plugin_list.append(handler_class())
2935- except (ImportError, AttributeError):
2936+ except NotImplementedError:
2937 # Skip missing plugins so that they can be ommitted from
2938 # installation if desired
2939 log("FetchHandler {} not found, skipping plugin".format(
2940
2941=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
2942--- hooks/charmhelpers/fetch/archiveurl.py 2015-01-27 14:54:02 +0000
2943+++ hooks/charmhelpers/fetch/archiveurl.py 2016-04-05 14:45:53 +0000
2944@@ -18,6 +18,16 @@
2945 import hashlib
2946 import re
2947
2948+from charmhelpers.fetch import (
2949+ BaseFetchHandler,
2950+ UnhandledSource
2951+)
2952+from charmhelpers.payload.archive import (
2953+ get_archive_handler,
2954+ extract,
2955+)
2956+from charmhelpers.core.host import mkdir, check_hash
2957+
2958 import six
2959 if six.PY3:
2960 from urllib.request import (
2961@@ -35,16 +45,6 @@
2962 )
2963 from urlparse import urlparse, urlunparse, parse_qs
2964
2965-from charmhelpers.fetch import (
2966- BaseFetchHandler,
2967- UnhandledSource
2968-)
2969-from charmhelpers.payload.archive import (
2970- get_archive_handler,
2971- extract,
2972-)
2973-from charmhelpers.core.host import mkdir, check_hash
2974-
2975
2976 def splituser(host):
2977 '''urllib.splituser(), but six's support of this seems broken'''
2978@@ -77,6 +77,8 @@
2979 def can_handle(self, source):
2980 url_parts = self.parse_url(source)
2981 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
2982+ # XXX: Why is this returning a boolean and a string? It's
2983+ # doomed to fail since "bool(can_handle('foo://'))" will be True.
2984 return "Wrong source type"
2985 if get_archive_handler(self.base_url(source)):
2986 return True
2987@@ -106,7 +108,7 @@
2988 install_opener(opener)
2989 response = urlopen(source)
2990 try:
2991- with open(dest, 'w') as dest_file:
2992+ with open(dest, 'wb') as dest_file:
2993 dest_file.write(response.read())
2994 except Exception as e:
2995 if os.path.isfile(dest):
2996@@ -155,7 +157,11 @@
2997 else:
2998 algorithms = hashlib.algorithms_available
2999 if key in algorithms:
3000- check_hash(dld_file, value, key)
3001+ if len(value) != 1:
3002+ raise TypeError(
3003+ "Expected 1 hash value, not %d" % len(value))
3004+ expected = value[0]
3005+ check_hash(dld_file, expected, key)
3006 if checksum:
3007 check_hash(dld_file, checksum, hash_type)
3008 return extract(dld_file, dest)
3009
3010=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
3011--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-27 14:54:02 +0000
3012+++ hooks/charmhelpers/fetch/bzrurl.py 2016-04-05 14:45:53 +0000
3013@@ -15,60 +15,54 @@
3014 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3015
3016 import os
3017+from subprocess import check_call, CalledProcessError
3018 from charmhelpers.fetch import (
3019 BaseFetchHandler,
3020- UnhandledSource
3021+ UnhandledSource,
3022+ filter_installed_packages,
3023+ apt_install,
3024 )
3025 from charmhelpers.core.host import mkdir
3026
3027-import six
3028-if six.PY3:
3029- raise ImportError('bzrlib does not support Python3')
3030
3031-try:
3032- from bzrlib.branch import Branch
3033- from bzrlib import bzrdir, workingtree, errors
3034-except ImportError:
3035- from charmhelpers.fetch import apt_install
3036- apt_install("python-bzrlib")
3037- from bzrlib.branch import Branch
3038- from bzrlib import bzrdir, workingtree, errors
3039+if filter_installed_packages(['bzr']) != []:
3040+ apt_install(['bzr'])
3041+ if filter_installed_packages(['bzr']) != []:
3042+ raise NotImplementedError('Unable to install bzr')
3043
3044
3045 class BzrUrlFetchHandler(BaseFetchHandler):
3046 """Handler for bazaar branches via generic and lp URLs"""
3047 def can_handle(self, source):
3048 url_parts = self.parse_url(source)
3049- if url_parts.scheme not in ('bzr+ssh', 'lp'):
3050+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
3051 return False
3052+ elif not url_parts.scheme:
3053+ return os.path.exists(os.path.join(source, '.bzr'))
3054 else:
3055 return True
3056
3057 def branch(self, source, dest):
3058- url_parts = self.parse_url(source)
3059- # If we use lp:branchname scheme we need to load plugins
3060 if not self.can_handle(source):
3061 raise UnhandledSource("Cannot handle {}".format(source))
3062- if url_parts.scheme == "lp":
3063- from bzrlib.plugin import load_plugins
3064- load_plugins()
3065- try:
3066- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
3067- except errors.AlreadyControlDirError:
3068- local_branch = Branch.open(dest)
3069- try:
3070- remote_branch = Branch.open(source)
3071- remote_branch.push(local_branch)
3072- tree = workingtree.WorkingTree.open(dest)
3073- tree.update()
3074- except Exception as e:
3075- raise e
3076-
3077- def install(self, source):
3078+
3079+ # if the target is a bzr repo, pull, else branch
3080+ try:
3081+ check_call(['bzr', 'revno', dest])
3082+ except CalledProcessError:
3083+ check_call(['bzr', 'branch', '--use-existing-dir', source, dest])
3084+ else:
3085+ check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
3086+
3087+ def install(self, source, dest=None):
3088 url_parts = self.parse_url(source)
3089 branch_name = url_parts.path.strip("/").split("/")[-1]
3090- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3091- branch_name)
3092+ if dest:
3093+ dest_dir = os.path.join(dest, branch_name)
3094+ else:
3095+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3096+ branch_name)
3097+
3098 if not os.path.exists(dest_dir):
3099 mkdir(dest_dir, perms=0o755)
3100 try:
3101
3102=== modified file 'hooks/charmhelpers/fetch/giturl.py'
3103--- hooks/charmhelpers/fetch/giturl.py 2015-01-27 14:54:02 +0000
3104+++ hooks/charmhelpers/fetch/giturl.py 2016-04-05 14:45:53 +0000
3105@@ -15,24 +15,18 @@
3106 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3107
3108 import os
3109+from subprocess import check_call, CalledProcessError
3110 from charmhelpers.fetch import (
3111 BaseFetchHandler,
3112- UnhandledSource
3113+ UnhandledSource,
3114+ filter_installed_packages,
3115+ apt_install,
3116 )
3117-from charmhelpers.core.host import mkdir
3118-
3119-import six
3120-if six.PY3:
3121- raise ImportError('GitPython does not support Python 3')
3122-
3123-try:
3124- from git import Repo
3125-except ImportError:
3126- from charmhelpers.fetch import apt_install
3127- apt_install("python-git")
3128- from git import Repo
3129-
3130-from git.exc import GitCommandError
3131+
3132+if filter_installed_packages(['git']) != []:
3133+ apt_install(['git'])
3134+ if filter_installed_packages(['git']) != []:
3135+ raise NotImplementedError('Unable to install git')
3136
3137
3138 class GitUrlFetchHandler(BaseFetchHandler):
3139@@ -40,19 +34,26 @@
3140 def can_handle(self, source):
3141 url_parts = self.parse_url(source)
3142 # TODO (mattyw) no support for ssh git@ yet
3143- if url_parts.scheme not in ('http', 'https', 'git'):
3144+ if url_parts.scheme not in ('http', 'https', 'git', ''):
3145 return False
3146+ elif not url_parts.scheme:
3147+ return os.path.exists(os.path.join(source, '.git'))
3148 else:
3149 return True
3150
3151- def clone(self, source, dest, branch):
3152+ def clone(self, source, dest, branch="master", depth=None):
3153 if not self.can_handle(source):
3154 raise UnhandledSource("Cannot handle {}".format(source))
3155
3156- repo = Repo.clone_from(source, dest)
3157- repo.git.checkout(branch)
3158+ if os.path.exists(dest):
3159+ cmd = ['git', '-C', dest, 'pull', source, branch]
3160+ else:
3161+ cmd = ['git', 'clone', source, dest, '--branch', branch]
3162+ if depth:
3163+ cmd.extend(['--depth', depth])
3164+ check_call(cmd)
3165
3166- def install(self, source, branch="master", dest=None):
3167+ def install(self, source, branch="master", dest=None, depth=None):
3168 url_parts = self.parse_url(source)
3169 branch_name = url_parts.path.strip("/").split("/")[-1]
3170 if dest:
3171@@ -60,12 +61,10 @@
3172 else:
3173 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3174 branch_name)
3175- if not os.path.exists(dest_dir):
3176- mkdir(dest_dir, perms=0o755)
3177 try:
3178- self.clone(source, dest_dir, branch)
3179- except GitCommandError as e:
3180- raise UnhandledSource(e.message)
3181+ self.clone(source, dest_dir, branch, depth)
3182+ except CalledProcessError as e:
3183+ raise UnhandledSource(e)
3184 except OSError as e:
3185 raise UnhandledSource(e.strerror)
3186 return dest_dir
3187
3188=== added directory 'hooks/diverted'
3189=== renamed symlink 'hooks/install' => 'hooks/diverted/install'
3190=== target changed u'hooks.py' => u'../hooks.py'
3191=== added file 'hooks/install'
3192--- hooks/install 1970-01-01 00:00:00 +0000
3193+++ hooks/install 2016-04-05 14:45:53 +0000
3194@@ -0,0 +1,7 @@
3195+#!/bin/bash
3196+# Wrapper to deal with newer Ubuntu versions that don't have py2 installed
3197+# by default.
3198+
3199+apt-get install -y python python-apt python-requests python-six python-yaml
3200+
3201+exec ./hooks/diverted/install
3202
3203=== modified file 'hooks/services.py'
3204--- hooks/services.py 2015-01-30 10:22:34 +0000
3205+++ hooks/services.py 2016-04-05 14:45:53 +0000
3206@@ -1,4 +1,5 @@
3207 import os
3208+from urlparse import urlparse
3209
3210 from charmhelpers.core import hookenv
3211 from charmhelpers.core.services.base import ServiceManager
3212@@ -15,6 +16,9 @@
3213 'service': 'wp-db',
3214 'required_data': [
3215 helpers.MysqlRelation(),
3216+ {
3217+ 'outbound_http_proxy': urlparse(config['outbound_http_proxy'])
3218+ },
3219 helpers.StoredContext('wp-secrets.json',
3220 actions.generate_secrets()),
3221 ],
3222@@ -61,7 +65,7 @@
3223 helpers.MysqlRelation(),
3224 wp_helpers.wordpress_configured(),
3225 helpers.RequiredConfig('akismet_key'),
3226- ],
3227+ ],
3228 'data_ready': [actions.enable_akismet],
3229 },
3230 {
3231@@ -102,34 +106,10 @@
3232 helpers.RequiredConfig(),
3233 ],
3234 'data_ready': [
3235- helpers.render_template(
3236- source='wp-nrpe.j2',
3237- target='/etc/nagios/nrpe.d/check_{}.cfg'.format(
3238- hookenv.local_unit().replace('/', '-'),
3239- )
3240- ),
3241- helpers.render_template(
3242- source='wp-nagios.j2',
3243- target='/var/lib/nagios/export/service__{}-{}.cfg'.format(
3244- config['nagios_context'],
3245- hookenv.local_unit().replace('/', '-'),
3246- )
3247- ),
3248+ actions.write_nrpe_checks
3249 ],
3250 'data_lost': [
3251- helpers.render_template(
3252- source='wp-nrpe.j2',
3253- target='/etc/nagios/nrpe.d/check_{}.cfg'.format(
3254- hookenv.local_unit().replace('/', '-'),
3255- )
3256- ),
3257- helpers.render_template(
3258- source='wp-nagios.j2',
3259- target='/var/lib/nagios/export/service__{}-{}.cfg'.format(
3260- config['nagios_context'],
3261- hookenv.local_unit().replace('/', '-'),
3262- )
3263- ),
3264+ actions.wipe_nrpe_checks
3265 ],
3266 },
3267 ])
3268
3269=== modified file 'hooks/wp_helpers.py'
3270--- hooks/wp_helpers.py 2015-02-25 17:05:27 +0000
3271+++ hooks/wp_helpers.py 2016-04-05 14:45:53 +0000
3272@@ -1,5 +1,6 @@
3273 import os
3274 import re
3275+import yaml
3276 import requests
3277 import urlparse
3278
3279@@ -36,6 +37,14 @@
3280 return []
3281
3282
3283+def get_vhost_options():
3284+ config = hookenv.config()
3285+ if config.get('vhost_options', False):
3286+ vhost_options = yaml.safe_load(config['vhost_options'])
3287+ return vhost_options
3288+ return None
3289+
3290+
3291 class PluginRelation(helpers.RelationContext):
3292 name = 'wordpress-plugin'
3293 interface = 'wordpress-plugin'
3294@@ -85,7 +94,9 @@
3295 urls = []
3296 hostnames = [config["blog_hostname"]]
3297 if config["additional_hostnames"]:
3298- hostnames.extend([h.strip() for h in config["additional_hostnames"].split(",")])
3299+ hostnames.extend(
3300+ [h.strip() for h in
3301+ config["additional_hostnames"].split(",")])
3302 for hostname in hostnames:
3303 urls.append(urlparse.ParseResult(
3304 'http',
3305@@ -102,11 +113,12 @@
3306 config = hookenv.config()
3307 services = []
3308 redirects = get_redirects()
3309+ extra_vhost_options = get_vhost_options()
3310 proxy = self.get_proxy()
3311 base_vhost = {"type": "php",
3312 "document_root": str(config["install_path"]),
3313 "webserver_options": ["mod_rewrite", "mod_headers"],
3314- "vhost_options": {'Header': 'append Vary "Cookie"'},
3315+ "vhost_options": [{'Header': 'append Vary "Cookie"'}],
3316 }
3317 for url in self.get_urls():
3318 vhost = base_vhost.copy()
3319@@ -115,6 +127,10 @@
3320 vhost["proxy"] = proxy
3321 if url.scheme == "http" and redirects:
3322 vhost["redirects"] = redirects
3323+ if "redirects" in config and config["redirects"]:
3324+ vhost["redirect_match"] = yaml.safe_load(config["redirects"])
3325+ if extra_vhost_options:
3326+ vhost["vhost_options"] = vhost["vhost_options"] + extra_vhost_options
3327 services.append(vhost)
3328
3329 return {'services': services}
3330@@ -177,8 +193,8 @@
3331 'ignore-must-revalidate',
3332 'ignore-private',
3333 'ignore-auth'
3334- ]
3335- }]
3336+ ]
3337+ }]
3338
3339 new_services = []
3340 for service in services:
3341@@ -196,6 +212,8 @@
3342 del(service['document_root'])
3343 if 'webserver_options' in service:
3344 del(service['webserver_options'])
3345+ if 'redirect_match' in service:
3346+ del(service['redirect_match'])
3347 new_services.append(service)
3348 return {'services': new_services}
3349
3350
3351=== modified file 'metadata.yaml'
3352--- metadata.yaml 2015-02-24 17:05:07 +0000
3353+++ metadata.yaml 2016-04-05 14:45:53 +0000
3354@@ -1,5 +1,6 @@
3355 name: wordpress
3356-maintainer: Nick Moffit <nick.moffit@canonical.com> Jacek Nykis <jacek.nykis@canonical.com>
3357+maintainer: Nick Moffit <nick.moffit@canonical.com>
3358+maintainer: Jacek Nykis <jacek.nykis@canonical.com>
3359 summary: "Wordpress"
3360 description: "Wordpress blog"
3361 categories: ["applications"]
3362
3363=== modified file 'templates/wp-apparmor.j2'
3364--- templates/wp-apparmor.j2 2014-12-11 18:10:02 +0000
3365+++ templates/wp-apparmor.j2 2016-04-05 14:45:53 +0000
3366@@ -15,6 +15,7 @@
3367 # Webserver subordinate will drop relevant rules here
3368 #include <webserver.d>
3369
3370+ @{PROC}/@{pid}/** r,
3371 {{install_path}}/ r,
3372 {{install_path}}/** r,
3373 {{install_path}}/wp-content/uploads/ rw,
3374
3375=== modified file 'templates/wp-info.php.j2'
3376--- templates/wp-info.php.j2 2015-01-29 15:48:37 +0000
3377+++ templates/wp-info.php.j2 2016-04-05 14:45:53 +0000
3378@@ -51,3 +51,16 @@
3379
3380 $table_prefix = 'wp_';
3381
3382+{% if outbound_http_proxy.hostname and outbound_http_proxy.port %}
3383+define('WP_PROXY_HOST', '{{outbound_http_proxy.hostname}}');
3384+define('WP_PROXY_PORT', '{{outbound_http_proxy.port}}');
3385+define('WP_PROXY_BYPASS_HOSTS', 'localhost');
3386+{% endif %}
3387+
3388+{% if outbound_http_proxy.username %}
3389+define('WP_PROXY_USERNAME', '{{outbound_http_proxy.username}}');
3390+{% endif %}
3391+
3392+{% if outbound_http_proxy.username and outbound_http_proxy.password %}
3393+define('WP_PROXY_PASSWORD', '{{outbound_http_proxy.password}}');
3394+{% endif %}
3395
3396=== removed file 'templates/wp-nagios.j2'
3397--- templates/wp-nagios.j2 2014-12-17 11:21:05 +0000
3398+++ templates/wp-nagios.j2 1970-01-01 00:00:00 +0000
3399@@ -1,48 +0,0 @@
3400-#
3401-# " "
3402-# mmm m m mmm m m
3403-# # # # # # #
3404-# # # # # # #
3405-# # "mm"# # "mm"#
3406-# # #
3407-# "" ""
3408-# This file is managed by Juju. Do not make local changes.
3409-
3410-{% macro servicegroup(default='juju') -%}
3411- {%- if config['nagios_servicegroups'] -%}
3412-{{ config['nagios_servicegroups'] }}
3413- {%- elif config['nagios_context'] -%}
3414-{{ config['nagios_context'] }}
3415- {%- else -%}
3416-{{ default }}
3417- {%- endif -%}
3418-{%- endmacro %}
3419-
3420-{% for rel in nrpe_external_master -%}
3421-define service {
3422- use active-service
3423- host_name {{rel.nagios_hostname}}
3424- service_description {{rel.nagios_hostname}}[wordpress_http] Check Wordpress HTTP
3425- check_command check_nrpe!check_wordpress_http
3426- servicegroups {{ servicegroup() }}
3427-}
3428-
3429- {% if config['ssl_enabled'] -%}
3430-define service {
3431- use active-service
3432- host_name {{rel.nagios_hostname}}
3433- service_description {{rel.nagios_hostname}}[wordpress_https] Check Wordpress HTTPS
3434- check_command check_nrpe!check_wordpress_https
3435- servicegroups {{ servicegroup() }}
3436-}
3437- {%- endif %}
3438- {% for plugin in wordpress_plugin %}
3439-define service {
3440- use active-service
3441- host_name {{rel.nagios_hostname}}
3442- service_description {{rel.nagios_hostname}}[{{plugin['plugin_name']|replace('-','_')}}] Check {{plugin['plugin_name']}} Wordpress Plug-In
3443- check_command check_nrpe!check_{{plugin['plugin_name']|replace('-','_')}}
3444- servicegroups {{ servicegroup() }}
3445-}
3446- {%- endfor %}
3447-{%- endfor %}
3448
3449=== removed file 'templates/wp-nrpe.j2'
3450--- templates/wp-nrpe.j2 2014-12-22 10:40:03 +0000
3451+++ templates/wp-nrpe.j2 1970-01-01 00:00:00 +0000
3452@@ -1,17 +0,0 @@
3453-#
3454-# " "
3455-# mmm m m mmm m m
3456-# # # # # # #
3457-# # # # # # #
3458-# # "mm"# # "mm"#
3459-# # #
3460-# "" ""
3461-# This file is managed by Juju. Do not make local changes.
3462-
3463-command[check_wordpress_http]=/usr/lib/nagios/plugins/check_http -I localhost -H {{config['blog_hostname']}} -p {{config['port_number']}} --onredirect=critical {% if config['nagios_check_string'] %} -s '{{config['nagios_check_string']}}'{% endif %}
3464-{% if config['ssl_enabled'] -%}
3465-command[check_wordpress_https]=/usr/lib/nagios/plugins/check_http -I localhost -H {{config['blog_hostname']}} -p {{config['ssl_port_number']}} -S --onredirect=critical {% if config['nagios_check_string'] %} -s '{{config['nagios_check_string']}}'{% endif %}
3466-{%- endif %}
3467-{%- for rel in wordpress_plugin %}
3468-command[check_{{rel['plugin_name']|replace('-','_')}}]=/usr/lib/nagios/plugins/check_file_age -w 31536000 -c 33696000 -f {{config['install_path']}}/wp-content/plugins/{{rel['plugin_name']}}
3469-{% endfor -%}

Subscribers

People subscribed via source and target branches