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 Sesquès
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 Pending
Review via email: mp+291000@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Laurent Sesquès (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)

Revision history for this message
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
=== added file 'Makefile'
--- Makefile 1970-01-01 00:00:00 +0000
+++ Makefile 2016-04-05 14:45:53 +0000
@@ -0,0 +1,15 @@
1#!/usr/bin/make
2HOOKS_DIR := $(PWD)/hooks
3TEST_PREFIX := PYTHONPATH=$(HOOKS_DIR)
4
5lint:
6 flake8 --exclude hooks/charmhelpers --ignore=E501 hooks tests
7 @charm proof
8
9bin/charm_helpers_sync.py:
10 @mkdir -p bin
11 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
12 > bin/charm_helpers_sync.py
13
14sync: bin/charm_helpers_sync.py
15 @python bin/charm_helpers_sync.py -c charm-helpers.yaml
016
=== modified file 'config.yaml'
--- config.yaml 2015-02-25 17:05:27 +0000
+++ config.yaml 2016-04-05 14:45:53 +0000
@@ -46,6 +46,12 @@
4646
47 If you're running multiple environments with the same services in them47 If you're running multiple environments with the same services in them
48 this allows you to differentiate between them.48 this allows you to differentiate between them.
49 nagios_servicegroups:
50 default: ""
51 type: string
52 description: |
53 A comma-separated list of nagios servicegroups.
54 If left empty, the nagios_context will be used as the servicegroup
49 nagios_check_string:55 nagios_check_string:
50 default: "Proudly powered by WordPress"56 default: "Proudly powered by WordPress"
51 type: string57 type: string
@@ -77,3 +83,34 @@
7783
78 If admin_password is not provided it will be automatically generated84 If admin_password is not provided it will be automatically generated
79 and stored on wordpress unit in the charm directory85 and stored on wordpress unit in the charm directory
86 outbound_http_proxy:
87 default: ""
88 type: string
89 description: >
90 Optional URL specifying a "forward" proxy to allow wordpress and its
91 plugins access to the Web. As an example:
92
93 outbound_http_proxy: http://user:pass@squid.example.com:3128/
94 redirects:
95 default: ""
96 type: string
97 description: >
98 Optional YAML formatted list of redirects that will be added to
99 apache vhost. For example setting this optino to:
100
101 [{"match": "(.*)\.gif$", "target": "http://example.com$1.jpg"},
102 {"match": "/old", "target": "http://example.com/new/", "type": "permanent"}]
103
104 Will result in the following configuration stanzas if apache2-subordinate is used:
105 RedirectMatch (.*)\.gif$ http://example.com$1.jpg
106 RedirectMatch permanent /old http://example.com/new
107 vhost_options:
108 default: ""
109 type: string
110 description: >
111 Optional YAML formatted list of additional virtual host config directives.
112 For example:
113
114 [{"Header": "append Cache-Control \"proxy-revalidate\""},
115 {"Header": "unset ETag"},
116 {"ExpiresDefault": "\"access plus 1 days\""}]
80117
=== added file 'files/wp-upgrade-check.php'
--- files/wp-upgrade-check.php 1970-01-01 00:00:00 +0000
+++ files/wp-upgrade-check.php 2016-04-05 14:45:53 +0000
@@ -0,0 +1,29 @@
1<?php
2
3require_once('wp-load.php');
4
5global $wp_version;
6$core_updates = 0;
7
8wp_version_check();
9
10$core = get_site_transient('update_core');
11
12foreach($core->updates as $update) {
13 if($update->current != $wp_version) {
14 $core_updates++;
15 }
16}
17
18if($core_updates) {
19 print("CRITICAL : $core_updates core updates available !\n");
20 exit(2);
21} else {
22 print("OK : no core update available\n");
23 exit(0);
24}
25
26print("CRITICAL : Error in " . __FILE__ . "\n");
27exit(2);
28
29?>
030
=== modified file 'hooks/actions.py'
--- hooks/actions.py 2015-02-25 17:05:27 +0000
+++ hooks/actions.py 2016-04-05 14:45:53 +0000
@@ -30,6 +30,8 @@
30 host.rsync(upstream_code + '/',30 host.rsync(upstream_code + '/',
31 config['install_path'],31 config['install_path'],
32 options=['--executability']) # Because we don't want --delete32 options=['--executability']) # Because we don't want --delete
33 host.rsync('files/wp-upgrade-check.php',
34 os.path.join(config['install_path'], 'wp-upgrade-check.php'))
33 host.mkdir('{}/wp-content/uploads'.format(config['install_path']),35 host.mkdir('{}/wp-content/uploads'.format(config['install_path']),
34 owner='www-data', perms=0755)36 owner='www-data', perms=0755)
35 if wp_helpers.wordpress_configured():37 if wp_helpers.wordpress_configured():
@@ -38,7 +40,10 @@
3840
3941
40def install_packages(service_name):42def install_packages(service_name):
41 packages = ['php5-cli', 'php5-mysql', 'php-symfony-yaml']43 if host.lsb_release()['DISTRIB_CODENAME'] == 'trusty':
44 packages = ['php5-cli', 'php5-mysql', 'php-symfony-yaml', 'php5-curl']
45 else:
46 packages = ['php-cli', 'php-mysql', 'php-symfony-yaml', 'php-curl', 'libapache2-mod-php']
42 fetch.apt_update()47 fetch.apt_update()
43 fetch.apt_install(packages)48 fetch.apt_install(packages)
4449
@@ -55,7 +60,7 @@
55 stdin=subprocess.PIPE,60 stdin=subprocess.PIPE,
56 stdout=subprocess.PIPE,61 stdout=subprocess.PIPE,
57 stderr=subprocess.STDOUT,62 stderr=subprocess.STDOUT,
58 )63 )
59 return process.communicate(stdin)[0] # spit back stdout+stderr combined64 return process.communicate(stdin)[0] # spit back stdout+stderr combined
6065
6166
@@ -122,7 +127,7 @@
122 """Perform initial configuratin of wordpress if needed."""127 """Perform initial configuratin of wordpress if needed."""
123 config = hookenv.config()128 config = hookenv.config()
124 if wp_helpers.wordpress_configured() or not config['initial_settings']:129 if wp_helpers.wordpress_configured() or not config['initial_settings']:
125 hookenv.log('No initial_setting provided or wordprass already '130 hookenv.log('No initial_setting provided or wordpress already '
126 'configured. Skipping first install.')131 'configured. Skipping first install.')
127 return132 return
128 hookenv.log('Starting wordpress initial configuration')133 hookenv.log('Starting wordpress initial configuration')
@@ -183,7 +188,7 @@
183def write_nrpe_checks(service_name):188def write_nrpe_checks(service_name):
184 config = hookenv.config()189 config = hookenv.config()
185 relation = wp_helpers.NEMRelation()['nrpe-external-master'][0]190 relation = wp_helpers.NEMRelation()['nrpe-external-master'][0]
186 nrpe = NRPE(hostname=relation['nagios_hostname'])191 nrpe = NRPE(hostname=relation['nagios_hostname'], primary=True)
187192
188 nrpe.add_check(193 nrpe.add_check(
189 shortname='wordpress_http',194 shortname='wordpress_http',
@@ -198,11 +203,36 @@
198 check_cmd='check_http -I localhost -H {} -p {} -S'.format(203 check_cmd='check_http -I localhost -H {} -p {} -S'.format(
199 config['blog_hostname'], config['ssl_port_number'])204 config['blog_hostname'], config['ssl_port_number'])
200 )205 )
206 nrpe.add_check(
207 shortname='wordpress_upgrades',
208 description='Check Wordpress core upgrades',
209 check_cmd='/usr/bin/php {}/wp-upgrade-check.php'.format(
210 config['install_path'])
211 )
212
213 plugin_relations = wp_helpers.PluginRelation()['wordpress-plugin']
214 for plugin in plugin_relations:
215 plugin_name = plugin.get('plugin_name')
216 if plugin_name:
217 nrpe.add_check(
218 shortname='wordpress_plugin_{}'.format(plugin_name),
219 description='Check Wordpress Plugin {}'.format(plugin_name),
220 check_cmd='check_file_age -w 31536000 -c 33696000 -f '
221 '{}/wp-content/plugins/{}'
222 ''.format(config['install_path'], plugin_name)
223
224 )
225
201 nrpe.write()226 nrpe.write()
202227
203228
204def wipe_nrpe_checks(service_name):229def wipe_nrpe_checks(service_name):
205 os.unlink('/var/lib/nagios/export/{}.cfg'.format(hookenv.local_unit()))230 for f in glob.glob('/var/lib/nagios/export/service__*wordpress_http?.cfg'):
231 if os.path.isfile(f):
232 os.unlink(f)
233 for f in glob.glob('/var/lib/nagios/export/service__*wordpress_plugin*.cfg'):
234 if os.path.isfile(f):
235 os.unlink(f)
206236
207237
208def apparmor_dirs(service_name):238def apparmor_dirs(service_name):
@@ -234,7 +264,7 @@
234 options were changed264 options were changed
235 """265 """
236 config = hookenv.config()266 config = hookenv.config()
237 options = ['port_number', 'ssl_enabled', 'ssl_port_number',267 options = ['port_number', 'ssl_enabled', 'ssl_port_number', 'redirects',
238 'install_path', 'blog_hostname', 'additional_hostnames']268 'install_path', 'blog_hostname', 'additional_hostnames']
239 if not (wp_helpers.object_storage()269 if not (wp_helpers.object_storage()
240 or any(config.changed(option) for option in options)):270 or any(config.changed(option) for option in options)):
241271
=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-04-05 14:45:53 +0000
@@ -24,6 +24,8 @@
24import pwd24import pwd
25import grp25import grp
26import os26import os
27import glob
28import shutil
27import re29import re
28import shlex30import shlex
29import yaml31import yaml
@@ -108,6 +110,13 @@
108# def local_monitors_relation_changed():110# def local_monitors_relation_changed():
109# update_nrpe_config()111# update_nrpe_config()
110#112#
113# 4.a If your charm is a subordinate charm set primary=False
114#
115# from charmsupport.nrpe import NRPE
116# (...)
117# def update_nrpe_config():
118# nrpe_compat = NRPE(primary=False)
119#
111# 5. ln -s hooks.py nrpe-external-master-relation-changed120# 5. ln -s hooks.py nrpe-external-master-relation-changed
112# ln -s hooks.py local-monitors-relation-changed121# ln -s hooks.py local-monitors-relation-changed
113122
@@ -146,6 +155,13 @@
146 self.description = description155 self.description = description
147 self.check_cmd = self._locate_cmd(check_cmd)156 self.check_cmd = self._locate_cmd(check_cmd)
148157
158 def _get_check_filename(self):
159 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
160
161 def _get_service_filename(self, hostname):
162 return os.path.join(NRPE.nagios_exportdir,
163 'service__{}_{}.cfg'.format(hostname, self.command))
164
149 def _locate_cmd(self, check_cmd):165 def _locate_cmd(self, check_cmd):
150 search_path = (166 search_path = (
151 '/usr/lib/nagios/plugins',167 '/usr/lib/nagios/plugins',
@@ -161,9 +177,21 @@
161 log('Check command not found: {}'.format(parts[0]))177 log('Check command not found: {}'.format(parts[0]))
162 return ''178 return ''
163179
164 def write(self, nagios_context, hostname, nagios_servicegroups=None):180 def _remove_service_files(self):
165 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(181 if not os.path.exists(NRPE.nagios_exportdir):
166 self.command)182 return
183 for f in os.listdir(NRPE.nagios_exportdir):
184 if f.endswith('_{}.cfg'.format(self.command)):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186
187 def remove(self, hostname):
188 nrpe_check_file = self._get_check_filename()
189 if os.path.exists(nrpe_check_file):
190 os.remove(nrpe_check_file)
191 self._remove_service_files()
192
193 def write(self, nagios_context, hostname, nagios_servicegroups):
194 nrpe_check_file = self._get_check_filename()
167 with open(nrpe_check_file, 'w') as nrpe_check_config:195 with open(nrpe_check_file, 'w') as nrpe_check_config:
168 nrpe_check_config.write("# check {}\n".format(self.shortname))196 nrpe_check_config.write("# check {}\n".format(self.shortname))
169 nrpe_check_config.write("command[{}]={}\n".format(197 nrpe_check_config.write("command[{}]={}\n".format(
@@ -177,13 +205,8 @@
177 nagios_servicegroups)205 nagios_servicegroups)
178206
179 def write_service_config(self, nagios_context, hostname,207 def write_service_config(self, nagios_context, hostname,
180 nagios_servicegroups=None):208 nagios_servicegroups):
181 for f in os.listdir(NRPE.nagios_exportdir):209 self._remove_service_files()
182 if re.search('.*{}.cfg'.format(self.command), f):
183 os.remove(os.path.join(NRPE.nagios_exportdir, f))
184
185 if not nagios_servicegroups:
186 nagios_servicegroups = nagios_context
187210
188 templ_vars = {211 templ_vars = {
189 'nagios_hostname': hostname,212 'nagios_hostname': hostname,
@@ -193,8 +216,7 @@
193 'command': self.command,216 'command': self.command,
194 }217 }
195 nrpe_service_text = Check.service_template.format(**templ_vars)218 nrpe_service_text = Check.service_template.format(**templ_vars)
196 nrpe_service_file = '{}/service__{}_{}.cfg'.format(219 nrpe_service_file = self._get_service_filename(hostname)
197 NRPE.nagios_exportdir, hostname, self.command)
198 with open(nrpe_service_file, 'w') as nrpe_service_config:220 with open(nrpe_service_file, 'w') as nrpe_service_config:
199 nrpe_service_config.write(str(nrpe_service_text))221 nrpe_service_config.write(str(nrpe_service_text))
200222
@@ -207,24 +229,51 @@
207 nagios_exportdir = '/var/lib/nagios/export'229 nagios_exportdir = '/var/lib/nagios/export'
208 nrpe_confdir = '/etc/nagios/nrpe.d'230 nrpe_confdir = '/etc/nagios/nrpe.d'
209231
210 def __init__(self, hostname=None):232 def __init__(self, hostname=None, primary=True):
211 super(NRPE, self).__init__()233 super(NRPE, self).__init__()
212 self.config = config()234 self.config = config()
235 self.primary = primary
213 self.nagios_context = self.config['nagios_context']236 self.nagios_context = self.config['nagios_context']
214 if 'nagios_servicegroups' in self.config:237 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
215 self.nagios_servicegroups = self.config['nagios_servicegroups']238 self.nagios_servicegroups = self.config['nagios_servicegroups']
216 else:239 else:
217 self.nagios_servicegroups = 'juju'240 self.nagios_servicegroups = self.nagios_context
218 self.unit_name = local_unit().replace('/', '-')241 self.unit_name = local_unit().replace('/', '-')
219 if hostname:242 if hostname:
220 self.hostname = hostname243 self.hostname = hostname
221 else:244 else:
222 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)245 nagios_hostname = get_nagios_hostname()
246 if nagios_hostname:
247 self.hostname = nagios_hostname
248 else:
249 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
223 self.checks = []250 self.checks = []
251 # Iff in an nrpe-external-master relation hook set primary status
252 relation = relation_ids()
253 if relation:
254 if relation[0].startswith('nrpe-external-master'):
255 log("Setting charm primary status {}".format(primary))
256 relation_set(relation_settings={'primary': self.primary})
224257
225 def add_check(self, *args, **kwargs):258 def add_check(self, *args, **kwargs):
226 self.checks.append(Check(*args, **kwargs))259 self.checks.append(Check(*args, **kwargs))
227260
261 def remove_check(self, *args, **kwargs):
262 if kwargs.get('shortname') is None:
263 raise ValueError('shortname of check must be specified')
264
265 # Use sensible defaults if they're not specified - these are not
266 # actually used during removal, but they're required for constructing
267 # the Check object; check_disk is chosen because it's part of the
268 # nagios-plugins-basic package.
269 if kwargs.get('check_cmd') is None:
270 kwargs['check_cmd'] = 'check_disk'
271 if kwargs.get('description') is None:
272 kwargs['description'] = ''
273
274 check = Check(*args, **kwargs)
275 check.remove(self.hostname)
276
228 def write(self):277 def write(self):
229 try:278 try:
230 nagios_uid = pwd.getpwnam('nagios').pw_uid279 nagios_uid = pwd.getpwnam('nagios').pw_uid
@@ -248,7 +297,9 @@
248297
249 service('restart', 'nagios-nrpe-server')298 service('restart', 'nagios-nrpe-server')
250299
251 for rid in relation_ids("local-monitors"):300 monitor_ids = relation_ids("local-monitors") + \
301 relation_ids("nrpe-external-master")
302 for rid in monitor_ids:
252 relation_set(relation_id=rid, monitors=yaml.dump(monitors))303 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
253304
254305
@@ -259,7 +310,7 @@
259 :param str relation_name: Name of relation nrpe sub joined to310 :param str relation_name: Name of relation nrpe sub joined to
260 """311 """
261 for rel in relations_of_type(relation_name):312 for rel in relations_of_type(relation_name):
262 if 'nagios_hostname' in rel:313 if 'nagios_host_context' in rel:
263 return rel['nagios_host_context']314 return rel['nagios_host_context']
264315
265316
@@ -300,11 +351,13 @@
300 upstart_init = '/etc/init/%s.conf' % svc351 upstart_init = '/etc/init/%s.conf' % svc
301 sysv_init = '/etc/init.d/%s' % svc352 sysv_init = '/etc/init.d/%s' % svc
302 if os.path.exists(upstart_init):353 if os.path.exists(upstart_init):
303 nrpe.add_check(354 # Don't add a check for these services from neutron-gateway
304 shortname=svc,355 if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
305 description='process check {%s}' % unit_name,356 nrpe.add_check(
306 check_cmd='check_upstart_job %s' % svc357 shortname=svc,
307 )358 description='process check {%s}' % unit_name,
359 check_cmd='check_upstart_job %s' % svc
360 )
308 elif os.path.exists(sysv_init):361 elif os.path.exists(sysv_init):
309 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc362 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
310 cron_file = ('*/5 * * * * root '363 cron_file = ('*/5 * * * * root '
@@ -322,3 +375,38 @@
322 check_cmd='check_status_file.py -f '375 check_cmd='check_status_file.py -f '
323 '/var/lib/nagios/service-check-%s.txt' % svc,376 '/var/lib/nagios/service-check-%s.txt' % svc,
324 )377 )
378
379
380def copy_nrpe_checks():
381 """
382 Copy the nrpe checks into place
383
384 """
385 NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
386 nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
387 'charmhelpers', 'contrib', 'openstack',
388 'files')
389
390 if not os.path.exists(NAGIOS_PLUGINS):
391 os.makedirs(NAGIOS_PLUGINS)
392 for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
393 if os.path.isfile(fname):
394 shutil.copy2(fname,
395 os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
396
397
398def add_haproxy_checks(nrpe, unit_name):
399 """
400 Add checks for each service in list
401
402 :param NRPE nrpe: NRPE object to add check to
403 :param str unit_name: Unit name to use in check description
404 """
405 nrpe.add_check(
406 shortname='haproxy_servers',
407 description='Check HAProxy {%s}' % unit_name,
408 check_cmd='check_haproxy.sh')
409 nrpe.add_check(
410 shortname='haproxy_queue',
411 description='Check HAProxy queue depth {%s}' % unit_name,
412 check_cmd='check_haproxy_queue_depth.sh')
325413
=== added file 'hooks/charmhelpers/core/files.py'
--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/files.py 2016-04-05 14:45:53 +0000
@@ -0,0 +1,45 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
21
22import os
23import subprocess
24
25
26def sed(filename, before, after, flags='g'):
27 """
28 Search and replaces the given pattern on filename.
29
30 :param filename: relative or absolute file path.
31 :param before: expression to be replaced (see 'man sed')
32 :param after: expression to replace with (see 'man sed')
33 :param flags: sed-compatible regex flags in example, to make
34 the search and replace case insensitive, specify ``flags="i"``.
35 The ``g`` flag is always specified regardless, so you do not
36 need to remember to include it when overriding this parameter.
37 :returns: If the sed command exit code was zero then return,
38 otherwise raise CalledProcessError.
39 """
40 expression = r's/{0}/{1}/{2}'.format(before,
41 after, flags)
42
43 return subprocess.check_call(["sed", "-i", "-r", "-e",
44 expression,
45 os.path.expanduser(filename)])
046
=== modified file 'hooks/charmhelpers/core/fstab.py'
--- hooks/charmhelpers/core/fstab.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/fstab.py 2016-04-05 14:45:53 +0000
@@ -17,11 +17,11 @@
17# You should have received a copy of the GNU Lesser General Public License17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
21
22import io20import io
23import os21import os
2422
23__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
24
2525
26class Fstab(io.FileIO):26class Fstab(io.FileIO):
27 """This class extends file in order to implement a file reader/writer27 """This class extends file in order to implement a file reader/writer
@@ -77,7 +77,7 @@
77 for line in self.readlines():77 for line in self.readlines():
78 line = line.decode('us-ascii')78 line = line.decode('us-ascii')
79 try:79 try:
80 if line.strip() and not line.startswith("#"):80 if line.strip() and not line.strip().startswith("#"):
81 yield self._hydrate_entry(line)81 yield self._hydrate_entry(line)
82 except ValueError:82 except ValueError:
83 pass83 pass
@@ -104,7 +104,7 @@
104104
105 found = False105 found = False
106 for index, line in enumerate(lines):106 for index, line in enumerate(lines):
107 if not line.startswith("#"):107 if line.strip() and not line.strip().startswith("#"):
108 if self._hydrate_entry(line) == entry:108 if self._hydrate_entry(line) == entry:
109 found = True109 found = True
110 break110 break
111111
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/hookenv.py 2016-04-05 14:45:53 +0000
@@ -20,11 +20,18 @@
20# Authors:20# Authors:
21# Charm Helpers Developers <juju@lists.ubuntu.com>21# Charm Helpers Developers <juju@lists.ubuntu.com>
2222
23from __future__ import print_function
24import copy
25from distutils.version import LooseVersion
26from functools import wraps
27import glob
23import os28import os
24import json29import json
25import yaml30import yaml
26import subprocess31import subprocess
27import sys32import sys
33import errno
34import tempfile
28from subprocess import CalledProcessError35from subprocess import CalledProcessError
2936
30import six37import six
@@ -56,15 +63,18 @@
5663
57 will cache the result of unit_get + 'test' for future calls.64 will cache the result of unit_get + 'test' for future calls.
58 """65 """
66 @wraps(func)
59 def wrapper(*args, **kwargs):67 def wrapper(*args, **kwargs):
60 global cache68 global cache
61 key = str((func, args, kwargs))69 key = str((func, args, kwargs))
62 try:70 try:
63 return cache[key]71 return cache[key]
64 except KeyError:72 except KeyError:
65 res = func(*args, **kwargs)73 pass # Drop out of the exception handler scope.
66 cache[key] = res74 res = func(*args, **kwargs)
67 return res75 cache[key] = res
76 return res
77 wrapper._wrapped = func
68 return wrapper78 return wrapper
6979
7080
@@ -87,7 +97,18 @@
87 if not isinstance(message, six.string_types):97 if not isinstance(message, six.string_types):
88 message = repr(message)98 message = repr(message)
89 command += [message]99 command += [message]
90 subprocess.call(command)100 # Missing juju-log should not cause failures in unit tests
101 # Send log output to stderr
102 try:
103 subprocess.call(command)
104 except OSError as e:
105 if e.errno == errno.ENOENT:
106 if level:
107 message = "{}: {}".format(level, message)
108 message = "juju-log: {}".format(message)
109 print(message, file=sys.stderr)
110 else:
111 raise
91112
92113
93class Serializable(UserDict):114class Serializable(UserDict):
@@ -153,9 +174,19 @@
153 return os.environ.get('JUJU_RELATION', None)174 return os.environ.get('JUJU_RELATION', None)
154175
155176
156def relation_id():177@cached
157 """The relation ID for the current relation hook"""178def relation_id(relation_name=None, service_or_unit=None):
158 return os.environ.get('JUJU_RELATION_ID', None)179 """The relation ID for the current or a specified relation"""
180 if not relation_name and not service_or_unit:
181 return os.environ.get('JUJU_RELATION_ID', None)
182 elif relation_name and service_or_unit:
183 service_name = service_or_unit.split('/')[0]
184 for relid in relation_ids(relation_name):
185 remote_service = remote_service_name(relid)
186 if remote_service == service_name:
187 return relid
188 else:
189 raise ValueError('Must specify neither or both of relation_name and service_or_unit')
159190
160191
161def local_unit():192def local_unit():
@@ -165,7 +196,7 @@
165196
166def remote_unit():197def remote_unit():
167 """The remote unit for the current relation hook"""198 """The remote unit for the current relation hook"""
168 return os.environ['JUJU_REMOTE_UNIT']199 return os.environ.get('JUJU_REMOTE_UNIT', None)
169200
170201
171def service_name():202def service_name():
@@ -173,9 +204,20 @@
173 return local_unit().split('/')[0]204 return local_unit().split('/')[0]
174205
175206
207@cached
208def remote_service_name(relid=None):
209 """The remote service name for a given relation-id (or the current relation)"""
210 if relid is None:
211 unit = remote_unit()
212 else:
213 units = related_units(relid)
214 unit = units[0] if units else None
215 return unit.split('/')[0] if unit else None
216
217
176def hook_name():218def hook_name():
177 """The name of the currently executing hook"""219 """The name of the currently executing hook"""
178 return os.path.basename(sys.argv[0])220 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
179221
180222
181class Config(dict):223class Config(dict):
@@ -225,23 +267,7 @@
225 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)267 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
226 if os.path.exists(self.path):268 if os.path.exists(self.path):
227 self.load_previous()269 self.load_previous()
228270 atexit(self._implicit_save)
229 def __getitem__(self, key):
230 """For regular dict lookups, check the current juju config first,
231 then the previous (saved) copy. This ensures that user-saved values
232 will be returned by a dict lookup.
233
234 """
235 try:
236 return dict.__getitem__(self, key)
237 except KeyError:
238 return (self._prev_dict or {})[key]
239
240 def keys(self):
241 prev_keys = []
242 if self._prev_dict is not None:
243 prev_keys = self._prev_dict.keys()
244 return list(set(prev_keys + list(dict.keys(self))))
245271
246 def load_previous(self, path=None):272 def load_previous(self, path=None):
247 """Load previous copy of config from disk.273 """Load previous copy of config from disk.
@@ -260,6 +286,9 @@
260 self.path = path or self.path286 self.path = path or self.path
261 with open(self.path) as f:287 with open(self.path) as f:
262 self._prev_dict = json.load(f)288 self._prev_dict = json.load(f)
289 for k, v in copy.deepcopy(self._prev_dict).items():
290 if k not in self:
291 self[k] = v
263292
264 def changed(self, key):293 def changed(self, key):
265 """Return True if the current value for this key is different from294 """Return True if the current value for this key is different from
@@ -291,13 +320,13 @@
291 instance.320 instance.
292321
293 """322 """
294 if self._prev_dict:
295 for k, v in six.iteritems(self._prev_dict):
296 if k not in self:
297 self[k] = v
298 with open(self.path, 'w') as f:323 with open(self.path, 'w') as f:
299 json.dump(self, f)324 json.dump(self, f)
300325
326 def _implicit_save(self):
327 if self.implicit_save:
328 self.save()
329
301330
302@cached331@cached
303def config(scope=None):332def config(scope=None):
@@ -340,18 +369,49 @@
340 """Set relation information for the current unit"""369 """Set relation information for the current unit"""
341 relation_settings = relation_settings if relation_settings else {}370 relation_settings = relation_settings if relation_settings else {}
342 relation_cmd_line = ['relation-set']371 relation_cmd_line = ['relation-set']
372 accepts_file = "--file" in subprocess.check_output(
373 relation_cmd_line + ["--help"], universal_newlines=True)
343 if relation_id is not None:374 if relation_id is not None:
344 relation_cmd_line.extend(('-r', relation_id))375 relation_cmd_line.extend(('-r', relation_id))
345 for k, v in (list(relation_settings.items()) + list(kwargs.items())):376 settings = relation_settings.copy()
346 if v is None:377 settings.update(kwargs)
347 relation_cmd_line.append('{}='.format(k))378 for key, value in settings.items():
348 else:379 # Force value to be a string: it always should, but some call
349 relation_cmd_line.append('{}={}'.format(k, v))380 # sites pass in things like dicts or numbers.
350 subprocess.check_call(relation_cmd_line)381 if value is not None:
382 settings[key] = "{}".format(value)
383 if accepts_file:
384 # --file was introduced in Juju 1.23.2. Use it by default if
385 # available, since otherwise we'll break if the relation data is
386 # too big. Ideally we should tell relation-set to read the data from
387 # stdin, but that feature is broken in 1.23.2: Bug #1454678.
388 with tempfile.NamedTemporaryFile(delete=False) as settings_file:
389 settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
390 subprocess.check_call(
391 relation_cmd_line + ["--file", settings_file.name])
392 os.remove(settings_file.name)
393 else:
394 for key, value in settings.items():
395 if value is None:
396 relation_cmd_line.append('{}='.format(key))
397 else:
398 relation_cmd_line.append('{}={}'.format(key, value))
399 subprocess.check_call(relation_cmd_line)
351 # Flush cache of any relation-gets for local unit400 # Flush cache of any relation-gets for local unit
352 flush(local_unit())401 flush(local_unit())
353402
354403
404def relation_clear(r_id=None):
405 ''' Clears any relation data already set on relation r_id '''
406 settings = relation_get(rid=r_id,
407 unit=local_unit())
408 for setting in settings:
409 if setting not in ['public-address', 'private-address']:
410 settings[setting] = None
411 relation_set(relation_id=r_id,
412 **settings)
413
414
355@cached415@cached
356def relation_ids(reltype=None):416def relation_ids(reltype=None):
357 """A list of relation_ids"""417 """A list of relation_ids"""
@@ -431,6 +491,76 @@
431491
432492
433@cached493@cached
494def peer_relation_id():
495 '''Get the peers relation id if a peers relation has been joined, else None.'''
496 md = metadata()
497 section = md.get('peers')
498 if section:
499 for key in section:
500 relids = relation_ids(key)
501 if relids:
502 return relids[0]
503 return None
504
505
506@cached
507def relation_to_interface(relation_name):
508 """
509 Given the name of a relation, return the interface that relation uses.
510
511 :returns: The interface name, or ``None``.
512 """
513 return relation_to_role_and_interface(relation_name)[1]
514
515
516@cached
517def relation_to_role_and_interface(relation_name):
518 """
519 Given the name of a relation, return the role and the name of the interface
520 that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
521
522 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
523 """
524 _metadata = metadata()
525 for role in ('provides', 'requires', 'peers'):
526 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
527 if interface:
528 return role, interface
529 return None, None
530
531
532@cached
533def role_and_interface_to_relations(role, interface_name):
534 """
535 Given a role and interface name, return a list of relation names for the
536 current charm that use that interface under that role (where role is one
537 of ``provides``, ``requires``, or ``peers``).
538
539 :returns: A list of relation names.
540 """
541 _metadata = metadata()
542 results = []
543 for relation_name, relation in _metadata.get(role, {}).items():
544 if relation['interface'] == interface_name:
545 results.append(relation_name)
546 return results
547
548
549@cached
550def interface_to_relations(interface_name):
551 """
552 Given an interface, return a list of relation names for the current
553 charm that use that interface.
554
555 :returns: A list of relation names.
556 """
557 results = []
558 for role in ('provides', 'requires', 'peers'):
559 results.extend(role_and_interface_to_relations(role, interface_name))
560 return results
561
562
563@cached
434def charm_name():564def charm_name():
435 """Get the name of the current charm as is specified on metadata.yaml"""565 """Get the name of the current charm as is specified on metadata.yaml"""
436 return metadata().get('name')566 return metadata().get('name')
@@ -496,11 +626,48 @@
496 return None626 return None
497627
498628
629def unit_public_ip():
630 """Get this unit's public IP address"""
631 return unit_get('public-address')
632
633
499def unit_private_ip():634def unit_private_ip():
500 """Get this unit's private IP address"""635 """Get this unit's private IP address"""
501 return unit_get('private-address')636 return unit_get('private-address')
502637
503638
639@cached
640def storage_get(attribute=None, storage_id=None):
641 """Get storage attributes"""
642 _args = ['storage-get', '--format=json']
643 if storage_id:
644 _args.extend(('-s', storage_id))
645 if attribute:
646 _args.append(attribute)
647 try:
648 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
649 except ValueError:
650 return None
651
652
653@cached
654def storage_list(storage_name=None):
655 """List the storage IDs for the unit"""
656 _args = ['storage-list', '--format=json']
657 if storage_name:
658 _args.append(storage_name)
659 try:
660 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
661 except ValueError:
662 return None
663 except OSError as e:
664 import errno
665 if e.errno == errno.ENOENT:
666 # storage-list does not exist
667 return []
668 raise
669
670
504class UnregisteredHookError(Exception):671class UnregisteredHookError(Exception):
505 """Raised when an undefined hook is called"""672 """Raised when an undefined hook is called"""
506 pass673 pass
@@ -528,10 +695,14 @@
528 hooks.execute(sys.argv)695 hooks.execute(sys.argv)
529 """696 """
530697
531 def __init__(self, config_save=True):698 def __init__(self, config_save=None):
532 super(Hooks, self).__init__()699 super(Hooks, self).__init__()
533 self._hooks = {}700 self._hooks = {}
534 self._config_save = config_save701
702 # For unknown reasons, we allow the Hooks constructor to override
703 # config().implicit_save.
704 if config_save is not None:
705 config().implicit_save = config_save
535706
536 def register(self, name, function):707 def register(self, name, function):
537 """Register a hook"""708 """Register a hook"""
@@ -539,13 +710,16 @@
539710
540 def execute(self, args):711 def execute(self, args):
541 """Execute a registered hook based on args[0]"""712 """Execute a registered hook based on args[0]"""
713 _run_atstart()
542 hook_name = os.path.basename(args[0])714 hook_name = os.path.basename(args[0])
543 if hook_name in self._hooks:715 if hook_name in self._hooks:
544 self._hooks[hook_name]()716 try:
545 if self._config_save:717 self._hooks[hook_name]()
546 cfg = config()718 except SystemExit as x:
547 if cfg.implicit_save:719 if x.code is None or x.code == 0:
548 cfg.save()720 _run_atexit()
721 raise
722 _run_atexit()
549 else:723 else:
550 raise UnregisteredHookError(hook_name)724 raise UnregisteredHookError(hook_name)
551725
@@ -566,3 +740,270 @@
566def charm_dir():740def charm_dir():
567 """Return the root directory of the current charm"""741 """Return the root directory of the current charm"""
568 return os.environ.get('CHARM_DIR')742 return os.environ.get('CHARM_DIR')
743
744
745@cached
746def action_get(key=None):
747 """Gets the value of an action parameter, or all key/value param pairs"""
748 cmd = ['action-get']
749 if key is not None:
750 cmd.append(key)
751 cmd.append('--format=json')
752 action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
753 return action_data
754
755
756def action_set(values):
757 """Sets the values to be returned after the action finishes"""
758 cmd = ['action-set']
759 for k, v in list(values.items()):
760 cmd.append('{}={}'.format(k, v))
761 subprocess.check_call(cmd)
762
763
764def action_fail(message):
765 """Sets the action status to failed and sets the error message.
766
767 The results set by action_set are preserved."""
768 subprocess.check_call(['action-fail', message])
769
770
771def action_name():
772 """Get the name of the currently executing action."""
773 return os.environ.get('JUJU_ACTION_NAME')
774
775
776def action_uuid():
777 """Get the UUID of the currently executing action."""
778 return os.environ.get('JUJU_ACTION_UUID')
779
780
781def action_tag():
782 """Get the tag for the currently executing action."""
783 return os.environ.get('JUJU_ACTION_TAG')
784
785
786def status_set(workload_state, message):
787 """Set the workload state with a message
788
789 Use status-set to set the workload state with a message which is visible
790 to the user via juju status. If the status-set command is not found then
791 assume this is juju < 1.23 and juju-log the message unstead.
792
793 workload_state -- valid juju workload state.
794 message -- status update message
795 """
796 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
797 if workload_state not in valid_states:
798 raise ValueError(
799 '{!r} is not a valid workload state'.format(workload_state)
800 )
801 cmd = ['status-set', workload_state, message]
802 try:
803 ret = subprocess.call(cmd)
804 if ret == 0:
805 return
806 except OSError as e:
807 if e.errno != errno.ENOENT:
808 raise
809 log_message = 'status-set failed: {} {}'.format(workload_state,
810 message)
811 log(log_message, level='INFO')
812
813
814def status_get():
815 """Retrieve the previously set juju workload state and message
816
817 If the status-get command is not found then assume this is juju < 1.23 and
818 return 'unknown', ""
819
820 """
821 cmd = ['status-get', "--format=json", "--include-data"]
822 try:
823 raw_status = subprocess.check_output(cmd)
824 except OSError as e:
825 if e.errno == errno.ENOENT:
826 return ('unknown', "")
827 else:
828 raise
829 else:
830 status = json.loads(raw_status.decode("UTF-8"))
831 return (status["status"], status["message"])
832
833
834def translate_exc(from_exc, to_exc):
835 def inner_translate_exc1(f):
836 @wraps(f)
837 def inner_translate_exc2(*args, **kwargs):
838 try:
839 return f(*args, **kwargs)
840 except from_exc:
841 raise to_exc
842
843 return inner_translate_exc2
844
845 return inner_translate_exc1
846
847
848@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
849def is_leader():
850 """Does the current unit hold the juju leadership
851
852 Uses juju to determine whether the current unit is the leader of its peers
853 """
854 cmd = ['is-leader', '--format=json']
855 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
856
857
858@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
859def leader_get(attribute=None):
860 """Juju leader get value(s)"""
861 cmd = ['leader-get', '--format=json'] + [attribute or '-']
862 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
863
864
865@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
866def leader_set(settings=None, **kwargs):
867 """Juju leader set value(s)"""
868 # Don't log secrets.
869 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
870 cmd = ['leader-set']
871 settings = settings or {}
872 settings.update(kwargs)
873 for k, v in settings.items():
874 if v is None:
875 cmd.append('{}='.format(k))
876 else:
877 cmd.append('{}={}'.format(k, v))
878 subprocess.check_call(cmd)
879
880
881@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
882def payload_register(ptype, klass, pid):
883 """ is used while a hook is running to let Juju know that a
884 payload has been started."""
885 cmd = ['payload-register']
886 for x in [ptype, klass, pid]:
887 cmd.append(x)
888 subprocess.check_call(cmd)
889
890
891@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
892def payload_unregister(klass, pid):
893 """ is used while a hook is running to let Juju know
894 that a payload has been manually stopped. The <class> and <id> provided
895 must match a payload that has been previously registered with juju using
896 payload-register."""
897 cmd = ['payload-unregister']
898 for x in [klass, pid]:
899 cmd.append(x)
900 subprocess.check_call(cmd)
901
902
903@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
904def payload_status_set(klass, pid, status):
905 """is used to update the current status of a registered payload.
906 The <class> and <id> provided must match a payload that has been previously
907 registered with juju using payload-register. The <status> must be one of the
908 follow: starting, started, stopping, stopped"""
909 cmd = ['payload-status-set']
910 for x in [klass, pid, status]:
911 cmd.append(x)
912 subprocess.check_call(cmd)
913
914
915@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
916def resource_get(name):
917 """used to fetch the resource path of the given name.
918
919 <name> must match a name of defined resource in metadata.yaml
920
921 returns either a path or False if resource not available
922 """
923 if not name:
924 return False
925
926 cmd = ['resource-get', name]
927 try:
928 return subprocess.check_output(cmd).decode('UTF-8')
929 except subprocess.CalledProcessError:
930 return False
931
932
933@cached
934def juju_version():
935 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
936 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
937 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
938 return subprocess.check_output([jujud, 'version'],
939 universal_newlines=True).strip()
940
941
942@cached
943def has_juju_version(minimum_version):
944 """Return True if the Juju version is at least the provided version"""
945 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
946
947
948_atexit = []
949_atstart = []
950
951
952def atstart(callback, *args, **kwargs):
953 '''Schedule a callback to run before the main hook.
954
955 Callbacks are run in the order they were added.
956
957 This is useful for modules and classes to perform initialization
958 and inject behavior. In particular:
959
960 - Run common code before all of your hooks, such as logging
961 the hook name or interesting relation data.
962 - Defer object or module initialization that requires a hook
963 context until we know there actually is a hook context,
964 making testing easier.
965 - Rather than requiring charm authors to include boilerplate to
966 invoke your helper's behavior, have it run automatically if
967 your object is instantiated or module imported.
968
969 This is not at all useful after your hook framework as been launched.
970 '''
971 global _atstart
972 _atstart.append((callback, args, kwargs))
973
974
975def atexit(callback, *args, **kwargs):
976 '''Schedule a callback to run on successful hook completion.
977
978 Callbacks are run in the reverse order that they were added.'''
979 _atexit.append((callback, args, kwargs))
980
981
982def _run_atstart():
983 '''Hook frameworks must invoke this before running the main hook body.'''
984 global _atstart
985 for callback, args, kwargs in _atstart:
986 callback(*args, **kwargs)
987 del _atstart[:]
988
989
990def _run_atexit():
991 '''Hook frameworks must invoke this after the main hook body has
992 successfully completed. Do not invoke it if the hook fails.'''
993 global _atexit
994 for callback, args, kwargs in reversed(_atexit):
995 callback(*args, **kwargs)
996 del _atexit[:]
997
998
999@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1000def network_get_primary_address(binding):
1001 '''
1002 Retrieve the primary network address for a named binding
1003
1004 :param binding: string. The name of a relation of extra-binding
1005 :return: string. The primary IP address for the named binding
1006 :raise: NotImplementedError if run on Juju < 2.0
1007 '''
1008 cmd = ['network-get', '--primary-address', binding]
1009 return subprocess.check_output(cmd).strip()
5691010
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/host.py 2016-04-05 14:45:53 +0000
@@ -24,11 +24,14 @@
24import os24import os
25import re25import re
26import pwd26import pwd
27import glob
27import grp28import grp
28import random29import random
29import string30import string
30import subprocess31import subprocess
31import hashlib32import hashlib
33import functools
34import itertools
32from contextlib import contextmanager35from contextlib import contextmanager
33from collections import OrderedDict36from collections import OrderedDict
3437
@@ -62,25 +65,86 @@
62 return service_result65 return service_result
6366
6467
68def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
69 """Pause a system service.
70
71 Stop it, and prevent it from starting again at boot."""
72 stopped = True
73 if service_running(service_name):
74 stopped = service_stop(service_name)
75 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
76 sysv_file = os.path.join(initd_dir, service_name)
77 if init_is_systemd():
78 service('disable', service_name)
79 elif os.path.exists(upstart_file):
80 override_path = os.path.join(
81 init_dir, '{}.override'.format(service_name))
82 with open(override_path, 'w') as fh:
83 fh.write("manual\n")
84 elif os.path.exists(sysv_file):
85 subprocess.check_call(["update-rc.d", service_name, "disable"])
86 else:
87 raise ValueError(
88 "Unable to detect {0} as SystemD, Upstart {1} or"
89 " SysV {2}".format(
90 service_name, upstart_file, sysv_file))
91 return stopped
92
93
94def service_resume(service_name, init_dir="/etc/init",
95 initd_dir="/etc/init.d"):
96 """Resume a system service.
97
98 Reenable starting again at boot. Start the service"""
99 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
100 sysv_file = os.path.join(initd_dir, service_name)
101 if init_is_systemd():
102 service('enable', service_name)
103 elif os.path.exists(upstart_file):
104 override_path = os.path.join(
105 init_dir, '{}.override'.format(service_name))
106 if os.path.exists(override_path):
107 os.unlink(override_path)
108 elif os.path.exists(sysv_file):
109 subprocess.check_call(["update-rc.d", service_name, "enable"])
110 else:
111 raise ValueError(
112 "Unable to detect {0} as SystemD, Upstart {1} or"
113 " SysV {2}".format(
114 service_name, upstart_file, sysv_file))
115
116 started = service_running(service_name)
117 if not started:
118 started = service_start(service_name)
119 return started
120
121
65def service(action, service_name):122def service(action, service_name):
66 """Control a system service"""123 """Control a system service"""
67 cmd = ['service', service_name, action]124 if init_is_systemd():
125 cmd = ['systemctl', action, service_name]
126 else:
127 cmd = ['service', service_name, action]
68 return subprocess.call(cmd) == 0128 return subprocess.call(cmd) == 0
69129
70130
71def service_running(service):131def service_running(service_name):
72 """Determine whether a system service is running"""132 """Determine whether a system service is running"""
73 try:133 if init_is_systemd():
74 output = subprocess.check_output(134 return service('is-active', service_name)
75 ['service', service, 'status'],
76 stderr=subprocess.STDOUT).decode('UTF-8')
77 except subprocess.CalledProcessError:
78 return False
79 else:135 else:
80 if ("start/running" in output or "is running" in output):136 try:
81 return True137 output = subprocess.check_output(
82 else:138 ['service', service_name, 'status'],
139 stderr=subprocess.STDOUT).decode('UTF-8')
140 except subprocess.CalledProcessError:
83 return False141 return False
142 else:
143 if ("start/running" in output or "is running" in output or
144 "up and running" in output):
145 return True
146 else:
147 return False
84148
85149
86def service_available(service_name):150def service_available(service_name):
@@ -90,13 +154,34 @@
90 ['service', service_name, 'status'],154 ['service', service_name, 'status'],
91 stderr=subprocess.STDOUT).decode('UTF-8')155 stderr=subprocess.STDOUT).decode('UTF-8')
92 except subprocess.CalledProcessError as e:156 except subprocess.CalledProcessError as e:
93 return 'unrecognized service' not in e.output157 return b'unrecognized service' not in e.output
94 else:158 else:
95 return True159 return True
96160
97161
98def adduser(username, password=None, shell='/bin/bash', system_user=False):162SYSTEMD_SYSTEM = '/run/systemd/system'
99 """Add a user to the system"""163
164
165def init_is_systemd():
166 """Return True if the host system uses systemd, False otherwise."""
167 return os.path.isdir(SYSTEMD_SYSTEM)
168
169
170def adduser(username, password=None, shell='/bin/bash', system_user=False,
171 primary_group=None, secondary_groups=None):
172 """Add a user to the system.
173
174 Will log but otherwise succeed if the user already exists.
175
176 :param str username: Username to create
177 :param str password: Password for user; if ``None``, create a system user
178 :param str shell: The default shell for the user
179 :param bool system_user: Whether to create a login or system user
180 :param str primary_group: Primary group for user; defaults to username
181 :param list secondary_groups: Optional list of additional groups
182
183 :returns: The password database entry struct, as returned by `pwd.getpwnam`
184 """
100 try:185 try:
101 user_info = pwd.getpwnam(username)186 user_info = pwd.getpwnam(username)
102 log('user {0} already exists!'.format(username))187 log('user {0} already exists!'.format(username))
@@ -111,12 +196,32 @@
111 '--shell', shell,196 '--shell', shell,
112 '--password', password,197 '--password', password,
113 ])198 ])
199 if not primary_group:
200 try:
201 grp.getgrnam(username)
202 primary_group = username # avoid "group exists" error
203 except KeyError:
204 pass
205 if primary_group:
206 cmd.extend(['-g', primary_group])
207 if secondary_groups:
208 cmd.extend(['-G', ','.join(secondary_groups)])
114 cmd.append(username)209 cmd.append(username)
115 subprocess.check_call(cmd)210 subprocess.check_call(cmd)
116 user_info = pwd.getpwnam(username)211 user_info = pwd.getpwnam(username)
117 return user_info212 return user_info
118213
119214
215def user_exists(username):
216 """Check if a user exists"""
217 try:
218 pwd.getpwnam(username)
219 user_exists = True
220 except KeyError:
221 user_exists = False
222 return user_exists
223
224
120def add_group(group_name, system_group=False):225def add_group(group_name, system_group=False):
121 """Add a group to the system"""226 """Add a group to the system"""
122 try:227 try:
@@ -139,11 +244,7 @@
139244
140def add_user_to_group(username, group):245def add_user_to_group(username, group):
141 """Add a user to a group"""246 """Add a user to a group"""
142 cmd = [247 cmd = ['gpasswd', '-a', username, group]
143 'gpasswd', '-a',
144 username,
145 group
146 ]
147 log("Adding user {} to group {}".format(username, group))248 log("Adding user {} to group {}".format(username, group))
148 subprocess.check_call(cmd)249 subprocess.check_call(cmd)
149250
@@ -191,25 +292,23 @@
191292
192293
193def write_file(path, content, owner='root', group='root', perms=0o444):294def write_file(path, content, owner='root', group='root', perms=0o444):
194 """Create or overwrite a file with the contents of a string"""295 """Create or overwrite a file with the contents of a byte string."""
195 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))296 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
196 uid = pwd.getpwnam(owner).pw_uid297 uid = pwd.getpwnam(owner).pw_uid
197 gid = grp.getgrnam(group).gr_gid298 gid = grp.getgrnam(group).gr_gid
198 with open(path, 'w') as target:299 with open(path, 'wb') as target:
199 os.fchown(target.fileno(), uid, gid)300 os.fchown(target.fileno(), uid, gid)
200 os.fchmod(target.fileno(), perms)301 os.fchmod(target.fileno(), perms)
201 target.write(content)302 target.write(content)
202303
203304
204def fstab_remove(mp):305def fstab_remove(mp):
205 """Remove the given mountpoint entry from /etc/fstab306 """Remove the given mountpoint entry from /etc/fstab"""
206 """
207 return Fstab.remove_by_mountpoint(mp)307 return Fstab.remove_by_mountpoint(mp)
208308
209309
210def fstab_add(dev, mp, fs, options=None):310def fstab_add(dev, mp, fs, options=None):
211 """Adds the given device entry to the /etc/fstab file311 """Adds the given device entry to the /etc/fstab file"""
212 """
213 return Fstab.add(dev, mp, fs, options=options)312 return Fstab.add(dev, mp, fs, options=options)
214313
215314
@@ -253,9 +352,19 @@
253 return system_mounts352 return system_mounts
254353
255354
355def fstab_mount(mountpoint):
356 """Mount filesystem using fstab"""
357 cmd_args = ['mount', mountpoint]
358 try:
359 subprocess.check_output(cmd_args)
360 except subprocess.CalledProcessError as e:
361 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
362 return False
363 return True
364
365
256def file_hash(path, hash_type='md5'):366def file_hash(path, hash_type='md5'):
257 """367 """Generate a hash checksum of the contents of 'path' or None if not found.
258 Generate a hash checksum of the contents of 'path' or None if not found.
259368
260 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,369 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
261 such as md5, sha1, sha256, sha512, etc.370 such as md5, sha1, sha256, sha512, etc.
@@ -269,9 +378,22 @@
269 return None378 return None
270379
271380
381def path_hash(path):
382 """Generate a hash checksum of all files matching 'path'. Standard
383 wildcards like '*' and '?' are supported, see documentation for the 'glob'
384 module for more information.
385
386 :return: dict: A { filename: hash } dictionary for all matched files.
387 Empty if none found.
388 """
389 return {
390 filename: file_hash(filename)
391 for filename in glob.iglob(path)
392 }
393
394
272def check_hash(path, checksum, hash_type='md5'):395def check_hash(path, checksum, hash_type='md5'):
273 """396 """Validate a file using a cryptographic checksum.
274 Validate a file using a cryptographic checksum.
275397
276 :param str checksum: Value of the checksum used to validate the file.398 :param str checksum: Value of the checksum used to validate the file.
277 :param str hash_type: Hash algorithm used to generate `checksum`.399 :param str hash_type: Hash algorithm used to generate `checksum`.
@@ -286,6 +408,7 @@
286408
287409
288class ChecksumError(ValueError):410class ChecksumError(ValueError):
411 """A class derived from Value error to indicate the checksum failed."""
289 pass412 pass
290413
291414
@@ -296,36 +419,58 @@
296419
297 @restart_on_change({420 @restart_on_change({
298 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]421 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
422 '/etc/apache/sites-enabled/*': [ 'apache2' ]
299 })423 })
300 def ceph_client_changed():424 def config_changed():
301 pass # your code here425 pass # your code here
302426
303 In this example, the cinder-api and cinder-volume services427 In this example, the cinder-api and cinder-volume services
304 would be restarted if /etc/ceph/ceph.conf is changed by the428 would be restarted if /etc/ceph/ceph.conf is changed by the
305 ceph_client_changed function.429 ceph_client_changed function. The apache2 service would be
430 restarted if any file matching the pattern got changed, created
431 or removed. Standard wildcards are supported, see documentation
432 for the 'glob' module for more information.
433
434 @param restart_map: {path_file_name: [service_name, ...]
435 @param stopstart: DEFAULT false; whether to stop, start OR restart
436 @returns result from decorated function
306 """437 """
307 def wrap(f):438 def wrap(f):
308 def wrapped_f(*args):439 @functools.wraps(f)
309 checksums = {}440 def wrapped_f(*args, **kwargs):
310 for path in restart_map:441 return restart_on_change_helper(
311 checksums[path] = file_hash(path)442 (lambda: f(*args, **kwargs)), restart_map, stopstart)
312 f(*args)
313 restarts = []
314 for path in restart_map:
315 if checksums[path] != file_hash(path):
316 restarts += restart_map[path]
317 services_list = list(OrderedDict.fromkeys(restarts))
318 if not stopstart:
319 for service_name in services_list:
320 service('restart', service_name)
321 else:
322 for action in ['stop', 'start']:
323 for service_name in services_list:
324 service(action, service_name)
325 return wrapped_f443 return wrapped_f
326 return wrap444 return wrap
327445
328446
447def restart_on_change_helper(lambda_f, restart_map, stopstart=False):
448 """Helper function to perform the restart_on_change function.
449
450 This is provided for decorators to restart services if files described
451 in the restart_map have changed after an invocation of lambda_f().
452
453 @param lambda_f: function to call.
454 @param restart_map: {file: [service, ...]}
455 @param stopstart: whether to stop, start or restart a service
456 @returns result of lambda_f()
457 """
458 checksums = {path: path_hash(path) for path in restart_map}
459 r = lambda_f()
460 # create a list of lists of the services to restart
461 restarts = [restart_map[path]
462 for path in restart_map
463 if path_hash(path) != checksums[path]]
464 # create a flat list of ordered services without duplicates from lists
465 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
466 if services_list:
467 actions = ('stop', 'start') if stopstart else ('restart',)
468 for action in actions:
469 for service_name in services_list:
470 service(action, service_name)
471 return r
472
473
329def lsb_release():474def lsb_release():
330 """Return /etc/lsb-release in a dict"""475 """Return /etc/lsb-release in a dict"""
331 d = {}476 d = {}
@@ -339,45 +484,105 @@
339def pwgen(length=None):484def pwgen(length=None):
340 """Generate a random pasword."""485 """Generate a random pasword."""
341 if length is None:486 if length is None:
487 # A random length is ok to use a weak PRNG
342 length = random.choice(range(35, 45))488 length = random.choice(range(35, 45))
343 alphanumeric_chars = [489 alphanumeric_chars = [
344 l for l in (string.ascii_letters + string.digits)490 l for l in (string.ascii_letters + string.digits)
345 if l not in 'l0QD1vAEIOUaeiou']491 if l not in 'l0QD1vAEIOUaeiou']
492 # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
493 # actual password
494 random_generator = random.SystemRandom()
346 random_chars = [495 random_chars = [
347 random.choice(alphanumeric_chars) for _ in range(length)]496 random_generator.choice(alphanumeric_chars) for _ in range(length)]
348 return(''.join(random_chars))497 return(''.join(random_chars))
349498
350499
351def list_nics(nic_type):500def is_phy_iface(interface):
352 '''Return a list of nics of given type(s)'''501 """Returns True if interface is not virtual, otherwise False."""
502 if interface:
503 sys_net = '/sys/class/net'
504 if os.path.isdir(sys_net):
505 for iface in glob.glob(os.path.join(sys_net, '*')):
506 if '/virtual/' in os.path.realpath(iface):
507 continue
508
509 if interface == os.path.basename(iface):
510 return True
511
512 return False
513
514
515def get_bond_master(interface):
516 """Returns bond master if interface is bond slave otherwise None.
517
518 NOTE: the provided interface is expected to be physical
519 """
520 if interface:
521 iface_path = '/sys/class/net/%s' % (interface)
522 if os.path.exists(iface_path):
523 if '/virtual/' in os.path.realpath(iface_path):
524 return None
525
526 master = os.path.join(iface_path, 'master')
527 if os.path.exists(master):
528 master = os.path.realpath(master)
529 # make sure it is a bond master
530 if os.path.exists(os.path.join(master, 'bonding')):
531 return os.path.basename(master)
532
533 return None
534
535
536def list_nics(nic_type=None):
537 """Return a list of nics of given type(s)"""
353 if isinstance(nic_type, six.string_types):538 if isinstance(nic_type, six.string_types):
354 int_types = [nic_type]539 int_types = [nic_type]
355 else:540 else:
356 int_types = nic_type541 int_types = nic_type
542
357 interfaces = []543 interfaces = []
358 for int_type in int_types:544 if nic_type:
359 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']545 for int_type in int_types:
546 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
547 ip_output = subprocess.check_output(cmd).decode('UTF-8')
548 ip_output = ip_output.split('\n')
549 ip_output = (line for line in ip_output if line)
550 for line in ip_output:
551 if line.split()[1].startswith(int_type):
552 matched = re.search('.*: (' + int_type +
553 r'[0-9]+\.[0-9]+)@.*', line)
554 if matched:
555 iface = matched.groups()[0]
556 else:
557 iface = line.split()[1].replace(":", "")
558
559 if iface not in interfaces:
560 interfaces.append(iface)
561 else:
562 cmd = ['ip', 'a']
360 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')563 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
361 ip_output = (line for line in ip_output if line)564 ip_output = (line.strip() for line in ip_output if line)
565
566 key = re.compile('^[0-9]+:\s+(.+):')
362 for line in ip_output:567 for line in ip_output:
363 if line.split()[1].startswith(int_type):568 matched = re.search(key, line)
364 matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)569 if matched:
365 if matched:570 iface = matched.group(1)
366 interface = matched.groups()[0]571 iface = iface.partition("@")[0]
367 else:572 if iface not in interfaces:
368 interface = line.split()[1].replace(":", "")573 interfaces.append(iface)
369 interfaces.append(interface)
370574
371 return interfaces575 return interfaces
372576
373577
374def set_nic_mtu(nic, mtu):578def set_nic_mtu(nic, mtu):
375 '''Set MTU on a network interface'''579 """Set the Maximum Transmission Unit (MTU) on a network interface."""
376 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]580 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
377 subprocess.check_call(cmd)581 subprocess.check_call(cmd)
378582
379583
380def get_nic_mtu(nic):584def get_nic_mtu(nic):
585 """Return the Maximum Transmission Unit (MTU) for a network interface."""
381 cmd = ['ip', 'addr', 'show', nic]586 cmd = ['ip', 'addr', 'show', nic]
382 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')587 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
383 mtu = ""588 mtu = ""
@@ -389,6 +594,7 @@
389594
390595
391def get_nic_hwaddr(nic):596def get_nic_hwaddr(nic):
597 """Return the Media Access Control (MAC) for a network interface."""
392 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]598 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
393 ip_output = subprocess.check_output(cmd).decode('UTF-8')599 ip_output = subprocess.check_output(cmd).decode('UTF-8')
394 hwaddr = ""600 hwaddr = ""
@@ -399,7 +605,7 @@
399605
400606
401def cmp_pkgrevno(package, revno, pkgcache=None):607def cmp_pkgrevno(package, revno, pkgcache=None):
402 '''Compare supplied revno with the revno of the installed package608 """Compare supplied revno with the revno of the installed package
403609
404 * 1 => Installed revno is greater than supplied arg610 * 1 => Installed revno is greater than supplied arg
405 * 0 => Installed revno is the same as supplied arg611 * 0 => Installed revno is the same as supplied arg
@@ -408,7 +614,7 @@
408 This function imports apt_cache function from charmhelpers.fetch if614 This function imports apt_cache function from charmhelpers.fetch if
409 the pkgcache argument is None. Be sure to add charmhelpers.fetch if615 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
410 you call this function, or pass an apt_pkg.Cache() instance.616 you call this function, or pass an apt_pkg.Cache() instance.
411 '''617 """
412 import apt_pkg618 import apt_pkg
413 if not pkgcache:619 if not pkgcache:
414 from charmhelpers.fetch import apt_cache620 from charmhelpers.fetch import apt_cache
@@ -418,15 +624,30 @@
418624
419625
420@contextmanager626@contextmanager
421def chdir(d):627def chdir(directory):
628 """Change the current working directory to a different directory for a code
629 block and return the previous directory after the block exits. Useful to
630 run commands from a specificed directory.
631
632 :param str directory: The directory path to change to for this context.
633 """
422 cur = os.getcwd()634 cur = os.getcwd()
423 try:635 try:
424 yield os.chdir(d)636 yield os.chdir(directory)
425 finally:637 finally:
426 os.chdir(cur)638 os.chdir(cur)
427639
428640
429def chownr(path, owner, group, follow_links=True):641def chownr(path, owner, group, follow_links=True, chowntopdir=False):
642 """Recursively change user and group ownership of files and directories
643 in given path. Doesn't chown path itself by default, only its children.
644
645 :param str path: The string path to start changing ownership.
646 :param str owner: The owner string to use when looking up the uid.
647 :param str group: The group string to use when looking up the gid.
648 :param bool follow_links: Also Chown links if True
649 :param bool chowntopdir: Also chown path itself if True
650 """
430 uid = pwd.getpwnam(owner).pw_uid651 uid = pwd.getpwnam(owner).pw_uid
431 gid = grp.getgrnam(group).gr_gid652 gid = grp.getgrnam(group).gr_gid
432 if follow_links:653 if follow_links:
@@ -434,6 +655,10 @@
434 else:655 else:
435 chown = os.lchown656 chown = os.lchown
436657
658 if chowntopdir:
659 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
660 if not broken_symlink:
661 chown(path, uid, gid)
437 for root, dirs, files in os.walk(path):662 for root, dirs, files in os.walk(path):
438 for name in dirs + files:663 for name in dirs + files:
439 full = os.path.join(root, name)664 full = os.path.join(root, name)
@@ -443,4 +668,28 @@
443668
444669
445def lchownr(path, owner, group):670def lchownr(path, owner, group):
671 """Recursively change user and group ownership of files and directories
672 in a given path, not following symbolic links. See the documentation for
673 'os.lchown' for more information.
674
675 :param str path: The string path to start changing ownership.
676 :param str owner: The owner string to use when looking up the uid.
677 :param str group: The group string to use when looking up the gid.
678 """
446 chownr(path, owner, group, follow_links=False)679 chownr(path, owner, group, follow_links=False)
680
681
682def get_total_ram():
683 """The total amount of system RAM in bytes.
684
685 This is what is reported by the OS, and may be overcommitted when
686 there are multiple containers hosted on the same machine.
687 """
688 with open('/proc/meminfo', 'r') as f:
689 for line in f.readlines():
690 if line:
691 key, value, unit = line.split()
692 if key == 'MemTotal:':
693 assert unit == 'kB', 'Unknown unit'
694 return int(value) * 1024 # Classic, not KiB.
695 raise NotImplementedError()
447696
=== added file 'hooks/charmhelpers/core/hugepage.py'
--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/hugepage.py 2016-04-05 14:45:53 +0000
@@ -0,0 +1,71 @@
1# -*- coding: utf-8 -*-
2
3# Copyright 2014-2015 Canonical Limited.
4#
5# This file is part of charm-helpers.
6#
7# charm-helpers is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser General Public License version 3 as
9# published by the Free Software Foundation.
10#
11# charm-helpers is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public License
17# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
19import yaml
20from charmhelpers.core import fstab
21from charmhelpers.core import sysctl
22from charmhelpers.core.host import (
23 add_group,
24 add_user_to_group,
25 fstab_mount,
26 mkdir,
27)
28from charmhelpers.core.strutils import bytes_from_string
29from subprocess import check_output
30
31
32def hugepage_support(user, group='hugetlb', nr_hugepages=256,
33 max_map_count=65536, mnt_point='/run/hugepages/kvm',
34 pagesize='2MB', mount=True, set_shmmax=False):
35 """Enable hugepages on system.
36
37 Args:
38 user (str) -- Username to allow access to hugepages to
39 group (str) -- Group name to own hugepages
40 nr_hugepages (int) -- Number of pages to reserve
41 max_map_count (int) -- Number of Virtual Memory Areas a process can own
42 mnt_point (str) -- Directory to mount hugepages on
43 pagesize (str) -- Size of hugepages
44 mount (bool) -- Whether to Mount hugepages
45 """
46 group_info = add_group(group)
47 gid = group_info.gr_gid
48 add_user_to_group(user, group)
49 if max_map_count < 2 * nr_hugepages:
50 max_map_count = 2 * nr_hugepages
51 sysctl_settings = {
52 'vm.nr_hugepages': nr_hugepages,
53 'vm.max_map_count': max_map_count,
54 'vm.hugetlb_shm_group': gid,
55 }
56 if set_shmmax:
57 shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
58 shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
59 if shmmax_minsize > shmmax_current:
60 sysctl_settings['kernel.shmmax'] = shmmax_minsize
61 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
62 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
63 lfstab = fstab.Fstab()
64 fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
65 if fstab_entry:
66 lfstab.remove_entry(fstab_entry)
67 entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
68 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
69 lfstab.add_entry(entry)
70 if mount:
71 fstab_mount(mnt_point)
072
=== added file 'hooks/charmhelpers/core/kernel.py'
--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel.py 2016-04-05 14:45:53 +0000
@@ -0,0 +1,68 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO
25)
26
27from subprocess import check_call, check_output
28import re
29
30
31def modprobe(module, persist=True):
32 """Load a kernel module and configure for auto-load on reboot."""
33 cmd = ['modprobe', module]
34
35 log('Loading kernel module %s' % module, level=INFO)
36
37 check_call(cmd)
38 if persist:
39 with open('/etc/modules', 'r+') as modules:
40 if module not in modules.read():
41 modules.write(module)
42
43
44def rmmod(module, force=False):
45 """Remove a module from the linux kernel"""
46 cmd = ['rmmod']
47 if force:
48 cmd.append('-f')
49 cmd.append(module)
50 log('Removing kernel module %s' % module, level=INFO)
51 return check_call(cmd)
52
53
54def lsmod():
55 """Shows what kernel modules are currently loaded"""
56 return check_output(['lsmod'],
57 universal_newlines=True)
58
59
60def is_module_loaded(module):
61 """Checks if a kernel module is already loaded"""
62 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
63 return len(matches) > 0
64
65
66def update_initramfs(version='all'):
67 """Updates an initramfs image"""
68 return check_call(["update-initramfs", "-k", version, "-u"])
069
=== modified file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/services/base.py 2016-04-05 14:45:53 +0000
@@ -15,9 +15,9 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18import re
19import json18import json
20from collections import Iterable19from inspect import getargspec
20from collections import Iterable, OrderedDict
2121
22from charmhelpers.core import host22from charmhelpers.core import host
23from charmhelpers.core import hookenv23from charmhelpers.core import hookenv
@@ -119,7 +119,7 @@
119 """119 """
120 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')120 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
121 self._ready = None121 self._ready = None
122 self.services = {}122 self.services = OrderedDict()
123 for service in services or []:123 for service in services or []:
124 service_name = service['service']124 service_name = service['service']
125 self.services[service_name] = service125 self.services[service_name] = service
@@ -128,15 +128,18 @@
128 """128 """
129 Handle the current hook by doing The Right Thing with the registered services.129 Handle the current hook by doing The Right Thing with the registered services.
130 """130 """
131 hook_name = hookenv.hook_name()131 hookenv._run_atstart()
132 if hook_name == 'stop':132 try:
133 self.stop_services()133 hook_name = hookenv.hook_name()
134 else:134 if hook_name == 'stop':
135 self.provide_data()135 self.stop_services()
136 self.reconfigure_services()136 else:
137 cfg = hookenv.config()137 self.reconfigure_services()
138 if cfg.implicit_save:138 self.provide_data()
139 cfg.save()139 except SystemExit as x:
140 if x.code is None or x.code == 0:
141 hookenv._run_atexit()
142 hookenv._run_atexit()
140143
141 def provide_data(self):144 def provide_data(self):
142 """145 """
@@ -145,15 +148,36 @@
145 A provider must have a `name` attribute, which indicates which relation148 A provider must have a `name` attribute, which indicates which relation
146 to set data on, and a `provide_data()` method, which returns a dict of149 to set data on, and a `provide_data()` method, which returns a dict of
147 data to set.150 data to set.
151
152 The `provide_data()` method can optionally accept two parameters:
153
154 * ``remote_service`` The name of the remote service that the data will
155 be provided to. The `provide_data()` method will be called once
156 for each connected service (not unit). This allows the method to
157 tailor its data to the given service.
158 * ``service_ready`` Whether or not the service definition had all of
159 its requirements met, and thus the ``data_ready`` callbacks run.
160
161 Note that the ``provided_data`` methods are now called **after** the
162 ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
163 a chance to generate any data necessary for the providing to the remote
164 services.
148 """165 """
149 hook_name = hookenv.hook_name()166 for service_name, service in self.services.items():
150 for service in self.services.values():167 service_ready = self.is_ready(service_name)
151 for provider in service.get('provided_data', []):168 for provider in service.get('provided_data', []):
152 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):169 for relid in hookenv.relation_ids(provider.name):
153 data = provider.provide_data()170 units = hookenv.related_units(relid)
154 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data171 if not units:
155 if _ready:172 continue
156 hookenv.relation_set(None, data)173 remote_service = units[0].split('/')[0]
174 argspec = getargspec(provider.provide_data)
175 if len(argspec.args) > 2:
176 data = provider.provide_data(remote_service, service_ready)
177 else:
178 data = provider.provide_data()
179 if data:
180 hookenv.relation_set(relid, data)
157181
158 def reconfigure_services(self, *service_names):182 def reconfigure_services(self, *service_names):
159 """183 """
160184
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2016-04-05 14:45:53 +0000
@@ -16,7 +16,9 @@
1616
17import os17import os
18import yaml18import yaml
19
19from charmhelpers.core import hookenv20from charmhelpers.core import hookenv
21from charmhelpers.core import host
20from charmhelpers.core import templating22from charmhelpers.core import templating
2123
22from charmhelpers.core.services.base import ManagerCallback24from charmhelpers.core.services.base import ManagerCallback
@@ -45,12 +47,14 @@
45 """47 """
46 name = None48 name = None
47 interface = None49 interface = None
48 required_keys = []
4950
50 def __init__(self, name=None, additional_required_keys=None):51 def __init__(self, name=None, additional_required_keys=None):
52 if not hasattr(self, 'required_keys'):
53 self.required_keys = []
54
51 if name is not None:55 if name is not None:
52 self.name = name56 self.name = name
53 if additional_required_keys is not None:57 if additional_required_keys:
54 self.required_keys.extend(additional_required_keys)58 self.required_keys.extend(additional_required_keys)
55 self.get_data()59 self.get_data()
5660
@@ -134,7 +138,10 @@
134 """138 """
135 name = 'db'139 name = 'db'
136 interface = 'mysql'140 interface = 'mysql'
137 required_keys = ['host', 'user', 'password', 'database']141
142 def __init__(self, *args, **kwargs):
143 self.required_keys = ['host', 'user', 'password', 'database']
144 RelationContext.__init__(self, *args, **kwargs)
138145
139146
140class HttpRelation(RelationContext):147class HttpRelation(RelationContext):
@@ -146,7 +153,10 @@
146 """153 """
147 name = 'website'154 name = 'website'
148 interface = 'http'155 interface = 'http'
149 required_keys = ['host', 'port']156
157 def __init__(self, *args, **kwargs):
158 self.required_keys = ['host', 'port']
159 RelationContext.__init__(self, *args, **kwargs)
150160
151 def provide_data(self):161 def provide_data(self):
152 return {162 return {
@@ -231,28 +241,51 @@
231 action.241 action.
232242
233 :param str source: The template source file, relative to243 :param str source: The template source file, relative to
234 `$CHARM_DIR/templates`244 `$CHARM_DIR/templates`
235245
236 :param str target: The target to write the rendered template to246 :param str target: The target to write the rendered template to (or None)
237 :param str owner: The owner of the rendered file247 :param str owner: The owner of the rendered file
238 :param str group: The group of the rendered file248 :param str group: The group of the rendered file
239 :param int perms: The permissions of the rendered file249 :param int perms: The permissions of the rendered file
250 :param partial on_change_action: functools partial to be executed when
251 rendered file changes
252 :param jinja2 loader template_loader: A jinja2 template loader
253
254 :return str: The rendered template
240 """255 """
241 def __init__(self, source, target,256 def __init__(self, source, target,
242 owner='root', group='root', perms=0o444):257 owner='root', group='root', perms=0o444,
258 on_change_action=None, template_loader=None):
243 self.source = source259 self.source = source
244 self.target = target260 self.target = target
245 self.owner = owner261 self.owner = owner
246 self.group = group262 self.group = group
247 self.perms = perms263 self.perms = perms
264 self.on_change_action = on_change_action
265 self.template_loader = template_loader
248266
249 def __call__(self, manager, service_name, event_name):267 def __call__(self, manager, service_name, event_name):
268 pre_checksum = ''
269 if self.on_change_action and os.path.isfile(self.target):
270 pre_checksum = host.file_hash(self.target)
250 service = manager.get_service(service_name)271 service = manager.get_service(service_name)
251 context = {}272 context = {'ctx': {}}
252 for ctx in service.get('required_data', []):273 for ctx in service.get('required_data', []):
253 context.update(ctx)274 context.update(ctx)
254 templating.render(self.source, self.target, context,275 context['ctx'].update(ctx)
255 self.owner, self.group, self.perms)276
277 result = templating.render(self.source, self.target, context,
278 self.owner, self.group, self.perms,
279 template_loader=self.template_loader)
280 if self.on_change_action:
281 if pre_checksum == host.file_hash(self.target):
282 hookenv.log(
283 'No change detected: {}'.format(self.target),
284 hookenv.DEBUG)
285 else:
286 self.on_change_action()
287
288 return result
256289
257290
258# Convenience aliases for templates291# Convenience aliases for templates
259292
=== added file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/strutils.py 2016-04-05 14:45:53 +0000
@@ -0,0 +1,72 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20import six
21import re
22
23
24def bool_from_string(value):
25 """Interpret string value as boolean.
26
27 Returns True if value translates to True otherwise False.
28 """
29 if isinstance(value, six.string_types):
30 value = six.text_type(value)
31 else:
32 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
33 raise ValueError(msg)
34
35 value = value.strip().lower()
36
37 if value in ['y', 'yes', 'true', 't', 'on']:
38 return True
39 elif value in ['n', 'no', 'false', 'f', 'off']:
40 return False
41
42 msg = "Unable to interpret string value '%s' as boolean" % (value)
43 raise ValueError(msg)
44
45
46def bytes_from_string(value):
47 """Interpret human readable string value as bytes.
48
49 Returns int
50 """
51 BYTE_POWER = {
52 'K': 1,
53 'KB': 1,
54 'M': 2,
55 'MB': 2,
56 'G': 3,
57 'GB': 3,
58 'T': 4,
59 'TB': 4,
60 'P': 5,
61 'PB': 5,
62 }
63 if isinstance(value, six.string_types):
64 value = six.text_type(value)
65 else:
66 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
67 raise ValueError(msg)
68 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
69 if not matches:
70 msg = "Unable to interpret string value '%s' as bytes" % (value)
71 raise ValueError(msg)
72 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
073
=== modified file 'hooks/charmhelpers/core/sysctl.py'
--- hooks/charmhelpers/core/sysctl.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/sysctl.py 2016-04-05 14:45:53 +0000
@@ -17,8 +17,6 @@
17# You should have received a copy of the GNU Lesser General Public License17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
21
22import yaml20import yaml
2321
24from subprocess import check_call22from subprocess import check_call
@@ -26,25 +24,33 @@
26from charmhelpers.core.hookenv import (24from charmhelpers.core.hookenv import (
27 log,25 log,
28 DEBUG,26 DEBUG,
27 ERROR,
29)28)
3029
30__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
31
3132
32def create(sysctl_dict, sysctl_file):33def create(sysctl_dict, sysctl_file):
33 """Creates a sysctl.conf file from a YAML associative array34 """Creates a sysctl.conf file from a YAML associative array
3435
35 :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }36 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
36 :type sysctl_dict: dict37 :type sysctl_dict: str
37 :param sysctl_file: path to the sysctl file to be saved38 :param sysctl_file: path to the sysctl file to be saved
38 :type sysctl_file: str or unicode39 :type sysctl_file: str or unicode
39 :returns: None40 :returns: None
40 """41 """
41 sysctl_dict = yaml.load(sysctl_dict)42 try:
43 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
44 except yaml.YAMLError:
45 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
46 level=ERROR)
47 return
4248
43 with open(sysctl_file, "w") as fd:49 with open(sysctl_file, "w") as fd:
44 for key, value in sysctl_dict.items():50 for key, value in sysctl_dict_parsed.items():
45 fd.write("{}={}\n".format(key, value))51 fd.write("{}={}\n".format(key, value))
4652
47 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),53 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
48 level=DEBUG)54 level=DEBUG)
4955
50 check_call(["sysctl", "-p", sysctl_file])56 check_call(["sysctl", "-p", sysctl_file])
5157
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/core/templating.py 2016-04-05 14:45:53 +0000
@@ -21,13 +21,14 @@
2121
2222
23def render(source, target, context, owner='root', group='root',23def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None):24 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
25 """25 """
26 Render a template.26 Render a template.
2727
28 The `source` path, if not absolute, is relative to the `templates_dir`.28 The `source` path, if not absolute, is relative to the `templates_dir`.
2929
30 The `target` path should be absolute.30 The `target` path should be absolute. It can also be `None`, in which
31 case no file will be written.
3132
32 The context should be a dict containing the values to be replaced in the33 The context should be a dict containing the values to be replaced in the
33 template.34 template.
@@ -36,6 +37,9 @@
3637
37 If omitted, `templates_dir` defaults to the `templates` folder in the charm.38 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
3839
40 The rendered template will be written to the file as well as being returned
41 as a string.
42
39 Note: Using this requires python-jinja2; if it is not installed, calling43 Note: Using this requires python-jinja2; if it is not installed, calling
40 this will attempt to use charmhelpers.fetch.apt_install to install it.44 this will attempt to use charmhelpers.fetch.apt_install to install it.
41 """45 """
@@ -52,17 +56,26 @@
52 apt_install('python-jinja2', fatal=True)56 apt_install('python-jinja2', fatal=True)
53 from jinja2 import FileSystemLoader, Environment, exceptions57 from jinja2 import FileSystemLoader, Environment, exceptions
5458
55 if templates_dir is None:59 if template_loader:
56 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')60 template_env = Environment(loader=template_loader)
57 loader = Environment(loader=FileSystemLoader(templates_dir))61 else:
62 if templates_dir is None:
63 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
64 template_env = Environment(loader=FileSystemLoader(templates_dir))
58 try:65 try:
59 source = source66 source = source
60 template = loader.get_template(source)67 template = template_env.get_template(source)
61 except exceptions.TemplateNotFound as e:68 except exceptions.TemplateNotFound as e:
62 hookenv.log('Could not load template %s from %s.' %69 hookenv.log('Could not load template %s from %s.' %
63 (source, templates_dir),70 (source, templates_dir),
64 level=hookenv.ERROR)71 level=hookenv.ERROR)
65 raise e72 raise e
66 content = template.render(context)73 content = template.render(context)
67 host.mkdir(os.path.dirname(target), owner, group)74 if target is not None:
68 host.write_file(target, content, owner, group, perms)75 target_dir = os.path.dirname(target)
76 if not os.path.exists(target_dir):
77 # This is a terrible default directory permission, as the file
78 # or its siblings will often contain secrets.
79 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
80 host.write_file(target, content.encode(encoding), owner, group, perms)
81 return content
6982
=== added file 'hooks/charmhelpers/core/unitdata.py'
--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/unitdata.py 2016-04-05 14:45:53 +0000
@@ -0,0 +1,521 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19#
20#
21# Authors:
22# Kapil Thangavelu <kapil.foss@gmail.com>
23#
24"""
25Intro
26-----
27
28A simple way to store state in units. This provides a key value
29storage with support for versioned, transactional operation,
30and can calculate deltas from previous values to simplify unit logic
31when processing changes.
32
33
34Hook Integration
35----------------
36
37There are several extant frameworks for hook execution, including
38
39 - charmhelpers.core.hookenv.Hooks
40 - charmhelpers.core.services.ServiceManager
41
42The storage classes are framework agnostic, one simple integration is
43via the HookData contextmanager. It will record the current hook
44execution environment (including relation data, config data, etc.),
45setup a transaction and allow easy access to the changes from
46previously seen values. One consequence of the integration is the
47reservation of particular keys ('rels', 'unit', 'env', 'config',
48'charm_revisions') for their respective values.
49
50Here's a fully worked integration example using hookenv.Hooks::
51
52 from charmhelper.core import hookenv, unitdata
53
54 hook_data = unitdata.HookData()
55 db = unitdata.kv()
56 hooks = hookenv.Hooks()
57
58 @hooks.hook
59 def config_changed():
60 # Print all changes to configuration from previously seen
61 # values.
62 for changed, (prev, cur) in hook_data.conf.items():
63 print('config changed', changed,
64 'previous value', prev,
65 'current value', cur)
66
67 # Get some unit specific bookeeping
68 if not db.get('pkg_key'):
69 key = urllib.urlopen('https://example.com/pkg_key').read()
70 db.set('pkg_key', key)
71
72 # Directly access all charm config as a mapping.
73 conf = db.getrange('config', True)
74
75 # Directly access all relation data as a mapping
76 rels = db.getrange('rels', True)
77
78 if __name__ == '__main__':
79 with hook_data():
80 hook.execute()
81
82
83A more basic integration is via the hook_scope context manager which simply
84manages transaction scope (and records hook name, and timestamp)::
85
86 >>> from unitdata import kv
87 >>> db = kv()
88 >>> with db.hook_scope('install'):
89 ... # do work, in transactional scope.
90 ... db.set('x', 1)
91 >>> db.get('x')
92 1
93
94
95Usage
96-----
97
98Values are automatically json de/serialized to preserve basic typing
99and complex data struct capabilities (dicts, lists, ints, booleans, etc).
100
101Individual values can be manipulated via get/set::
102
103 >>> kv.set('y', True)
104 >>> kv.get('y')
105 True
106
107 # We can set complex values (dicts, lists) as a single key.
108 >>> kv.set('config', {'a': 1, 'b': True'})
109
110 # Also supports returning dictionaries as a record which
111 # provides attribute access.
112 >>> config = kv.get('config', record=True)
113 >>> config.b
114 True
115
116
117Groups of keys can be manipulated with update/getrange::
118
119 >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
120 >>> kv.getrange('gui.', strip=True)
121 {'z': 1, 'y': 2}
122
123When updating values, its very helpful to understand which values
124have actually changed and how have they changed. The storage
125provides a delta method to provide for this::
126
127 >>> data = {'debug': True, 'option': 2}
128 >>> delta = kv.delta(data, 'config.')
129 >>> delta.debug.previous
130 None
131 >>> delta.debug.current
132 True
133 >>> delta
134 {'debug': (None, True), 'option': (None, 2)}
135
136Note the delta method does not persist the actual change, it needs to
137be explicitly saved via 'update' method::
138
139 >>> kv.update(data, 'config.')
140
141Values modified in the context of a hook scope retain historical values
142associated to the hookname.
143
144 >>> with db.hook_scope('config-changed'):
145 ... db.set('x', 42)
146 >>> db.gethistory('x')
147 [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
148 (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
149
150"""
151
152import collections
153import contextlib
154import datetime
155import itertools
156import json
157import os
158import pprint
159import sqlite3
160import sys
161
162__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
163
164
165class Storage(object):
166 """Simple key value database for local unit state within charms.
167
168 Modifications are not persisted unless :meth:`flush` is called.
169
170 To support dicts, lists, integer, floats, and booleans values
171 are automatically json encoded/decoded.
172 """
173 def __init__(self, path=None):
174 self.db_path = path
175 if path is None:
176 if 'UNIT_STATE_DB' in os.environ:
177 self.db_path = os.environ['UNIT_STATE_DB']
178 else:
179 self.db_path = os.path.join(
180 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
181 self.conn = sqlite3.connect('%s' % self.db_path)
182 self.cursor = self.conn.cursor()
183 self.revision = None
184 self._closed = False
185 self._init()
186
187 def close(self):
188 if self._closed:
189 return
190 self.flush(False)
191 self.cursor.close()
192 self.conn.close()
193 self._closed = True
194
195 def get(self, key, default=None, record=False):
196 self.cursor.execute('select data from kv where key=?', [key])
197 result = self.cursor.fetchone()
198 if not result:
199 return default
200 if record:
201 return Record(json.loads(result[0]))
202 return json.loads(result[0])
203
204 def getrange(self, key_prefix, strip=False):
205 """
206 Get a range of keys starting with a common prefix as a mapping of
207 keys to values.
208
209 :param str key_prefix: Common prefix among all keys
210 :param bool strip: Optionally strip the common prefix from the key
211 names in the returned dict
212 :return dict: A (possibly empty) dict of key-value mappings
213 """
214 self.cursor.execute("select key, data from kv where key like ?",
215 ['%s%%' % key_prefix])
216 result = self.cursor.fetchall()
217
218 if not result:
219 return {}
220 if not strip:
221 key_prefix = ''
222 return dict([
223 (k[len(key_prefix):], json.loads(v)) for k, v in result])
224
225 def update(self, mapping, prefix=""):
226 """
227 Set the values of multiple keys at once.
228
229 :param dict mapping: Mapping of keys to values
230 :param str prefix: Optional prefix to apply to all keys in `mapping`
231 before setting
232 """
233 for k, v in mapping.items():
234 self.set("%s%s" % (prefix, k), v)
235
236 def unset(self, key):
237 """
238 Remove a key from the database entirely.
239 """
240 self.cursor.execute('delete from kv where key=?', [key])
241 if self.revision and self.cursor.rowcount:
242 self.cursor.execute(
243 'insert into kv_revisions values (?, ?, ?)',
244 [key, self.revision, json.dumps('DELETED')])
245
246 def unsetrange(self, keys=None, prefix=""):
247 """
248 Remove a range of keys starting with a common prefix, from the database
249 entirely.
250
251 :param list keys: List of keys to remove.
252 :param str prefix: Optional prefix to apply to all keys in ``keys``
253 before removing.
254 """
255 if keys is not None:
256 keys = ['%s%s' % (prefix, key) for key in keys]
257 self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
258 if self.revision and self.cursor.rowcount:
259 self.cursor.execute(
260 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
261 list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
262 else:
263 self.cursor.execute('delete from kv where key like ?',
264 ['%s%%' % prefix])
265 if self.revision and self.cursor.rowcount:
266 self.cursor.execute(
267 'insert into kv_revisions values (?, ?, ?)',
268 ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
269
270 def set(self, key, value):
271 """
272 Set a value in the database.
273
274 :param str key: Key to set the value for
275 :param value: Any JSON-serializable value to be set
276 """
277 serialized = json.dumps(value)
278
279 self.cursor.execute('select data from kv where key=?', [key])
280 exists = self.cursor.fetchone()
281
282 # Skip mutations to the same value
283 if exists:
284 if exists[0] == serialized:
285 return value
286
287 if not exists:
288 self.cursor.execute(
289 'insert into kv (key, data) values (?, ?)',
290 (key, serialized))
291 else:
292 self.cursor.execute('''
293 update kv
294 set data = ?
295 where key = ?''', [serialized, key])
296
297 # Save
298 if not self.revision:
299 return value
300
301 self.cursor.execute(
302 'select 1 from kv_revisions where key=? and revision=?',
303 [key, self.revision])
304 exists = self.cursor.fetchone()
305
306 if not exists:
307 self.cursor.execute(
308 '''insert into kv_revisions (
309 revision, key, data) values (?, ?, ?)''',
310 (self.revision, key, serialized))
311 else:
312 self.cursor.execute(
313 '''
314 update kv_revisions
315 set data = ?
316 where key = ?
317 and revision = ?''',
318 [serialized, key, self.revision])
319
320 return value
321
322 def delta(self, mapping, prefix):
323 """
324 return a delta containing values that have changed.
325 """
326 previous = self.getrange(prefix, strip=True)
327 if not previous:
328 pk = set()
329 else:
330 pk = set(previous.keys())
331 ck = set(mapping.keys())
332 delta = DeltaSet()
333
334 # added
335 for k in ck.difference(pk):
336 delta[k] = Delta(None, mapping[k])
337
338 # removed
339 for k in pk.difference(ck):
340 delta[k] = Delta(previous[k], None)
341
342 # changed
343 for k in pk.intersection(ck):
344 c = mapping[k]
345 p = previous[k]
346 if c != p:
347 delta[k] = Delta(p, c)
348
349 return delta
350
351 @contextlib.contextmanager
352 def hook_scope(self, name=""):
353 """Scope all future interactions to the current hook execution
354 revision."""
355 assert not self.revision
356 self.cursor.execute(
357 'insert into hooks (hook, date) values (?, ?)',
358 (name or sys.argv[0],
359 datetime.datetime.utcnow().isoformat()))
360 self.revision = self.cursor.lastrowid
361 try:
362 yield self.revision
363 self.revision = None
364 except:
365 self.flush(False)
366 self.revision = None
367 raise
368 else:
369 self.flush()
370
371 def flush(self, save=True):
372 if save:
373 self.conn.commit()
374 elif self._closed:
375 return
376 else:
377 self.conn.rollback()
378
379 def _init(self):
380 self.cursor.execute('''
381 create table if not exists kv (
382 key text,
383 data text,
384 primary key (key)
385 )''')
386 self.cursor.execute('''
387 create table if not exists kv_revisions (
388 key text,
389 revision integer,
390 data text,
391 primary key (key, revision)
392 )''')
393 self.cursor.execute('''
394 create table if not exists hooks (
395 version integer primary key autoincrement,
396 hook text,
397 date text
398 )''')
399 self.conn.commit()
400
401 def gethistory(self, key, deserialize=False):
402 self.cursor.execute(
403 '''
404 select kv.revision, kv.key, kv.data, h.hook, h.date
405 from kv_revisions kv,
406 hooks h
407 where kv.key=?
408 and kv.revision = h.version
409 ''', [key])
410 if deserialize is False:
411 return self.cursor.fetchall()
412 return map(_parse_history, self.cursor.fetchall())
413
414 def debug(self, fh=sys.stderr):
415 self.cursor.execute('select * from kv')
416 pprint.pprint(self.cursor.fetchall(), stream=fh)
417 self.cursor.execute('select * from kv_revisions')
418 pprint.pprint(self.cursor.fetchall(), stream=fh)
419
420
421def _parse_history(d):
422 return (d[0], d[1], json.loads(d[2]), d[3],
423 datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
424
425
426class HookData(object):
427 """Simple integration for existing hook exec frameworks.
428
429 Records all unit information, and stores deltas for processing
430 by the hook.
431
432 Sample::
433
434 from charmhelper.core import hookenv, unitdata
435
436 changes = unitdata.HookData()
437 db = unitdata.kv()
438 hooks = hookenv.Hooks()
439
440 @hooks.hook
441 def config_changed():
442 # View all changes to configuration
443 for changed, (prev, cur) in changes.conf.items():
444 print('config changed', changed,
445 'previous value', prev,
446 'current value', cur)
447
448 # Get some unit specific bookeeping
449 if not db.get('pkg_key'):
450 key = urllib.urlopen('https://example.com/pkg_key').read()
451 db.set('pkg_key', key)
452
453 if __name__ == '__main__':
454 with changes():
455 hook.execute()
456
457 """
458 def __init__(self):
459 self.kv = kv()
460 self.conf = None
461 self.rels = None
462
463 @contextlib.contextmanager
464 def __call__(self):
465 from charmhelpers.core import hookenv
466 hook_name = hookenv.hook_name()
467
468 with self.kv.hook_scope(hook_name):
469 self._record_charm_version(hookenv.charm_dir())
470 delta_config, delta_relation = self._record_hook(hookenv)
471 yield self.kv, delta_config, delta_relation
472
473 def _record_charm_version(self, charm_dir):
474 # Record revisions.. charm revisions are meaningless
475 # to charm authors as they don't control the revision.
476 # so logic dependnent on revision is not particularly
477 # useful, however it is useful for debugging analysis.
478 charm_rev = open(
479 os.path.join(charm_dir, 'revision')).read().strip()
480 charm_rev = charm_rev or '0'
481 revs = self.kv.get('charm_revisions', [])
482 if charm_rev not in revs:
483 revs.append(charm_rev.strip() or '0')
484 self.kv.set('charm_revisions', revs)
485
486 def _record_hook(self, hookenv):
487 data = hookenv.execution_environment()
488 self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
489 self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
490 self.kv.set('env', dict(data['env']))
491 self.kv.set('unit', data['unit'])
492 self.kv.set('relid', data.get('relid'))
493 return conf_delta, rels_delta
494
495
496class Record(dict):
497
498 __slots__ = ()
499
500 def __getattr__(self, k):
501 if k in self:
502 return self[k]
503 raise AttributeError(k)
504
505
506class DeltaSet(Record):
507
508 __slots__ = ()
509
510
511Delta = collections.namedtuple('Delta', ['previous', 'current'])
512
513
514_KV = None
515
516
517def kv():
518 global _KV
519 if _KV is None:
520 _KV = Storage()
521 return _KV
0522
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2016-04-05 14:45:53 +0000
@@ -90,6 +90,22 @@
90 'kilo/proposed': 'trusty-proposed/kilo',90 'kilo/proposed': 'trusty-proposed/kilo',
91 'trusty-kilo/proposed': 'trusty-proposed/kilo',91 'trusty-kilo/proposed': 'trusty-proposed/kilo',
92 'trusty-proposed/kilo': 'trusty-proposed/kilo',92 'trusty-proposed/kilo': 'trusty-proposed/kilo',
93 # Liberty
94 'liberty': 'trusty-updates/liberty',
95 'trusty-liberty': 'trusty-updates/liberty',
96 'trusty-liberty/updates': 'trusty-updates/liberty',
97 'trusty-updates/liberty': 'trusty-updates/liberty',
98 'liberty/proposed': 'trusty-proposed/liberty',
99 'trusty-liberty/proposed': 'trusty-proposed/liberty',
100 'trusty-proposed/liberty': 'trusty-proposed/liberty',
101 # Mitaka
102 'mitaka': 'trusty-updates/mitaka',
103 'trusty-mitaka': 'trusty-updates/mitaka',
104 'trusty-mitaka/updates': 'trusty-updates/mitaka',
105 'trusty-updates/mitaka': 'trusty-updates/mitaka',
106 'mitaka/proposed': 'trusty-proposed/mitaka',
107 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
108 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
93}109}
94110
95# The order of this list is very important. Handlers should be listed in from111# The order of this list is very important. Handlers should be listed in from
@@ -158,7 +174,7 @@
158174
159def apt_cache(in_memory=True):175def apt_cache(in_memory=True):
160 """Build and return an apt cache"""176 """Build and return an apt cache"""
161 import apt_pkg177 from apt import apt_pkg
162 apt_pkg.init()178 apt_pkg.init()
163 if in_memory:179 if in_memory:
164 apt_pkg.config.set("Dir::Cache::pkgcache", "")180 apt_pkg.config.set("Dir::Cache::pkgcache", "")
@@ -215,19 +231,27 @@
215 _run_apt_command(cmd, fatal)231 _run_apt_command(cmd, fatal)
216232
217233
234def apt_mark(packages, mark, fatal=False):
235 """Flag one or more packages using apt-mark"""
236 log("Marking {} as {}".format(packages, mark))
237 cmd = ['apt-mark', mark]
238 if isinstance(packages, six.string_types):
239 cmd.append(packages)
240 else:
241 cmd.extend(packages)
242
243 if fatal:
244 subprocess.check_call(cmd, universal_newlines=True)
245 else:
246 subprocess.call(cmd, universal_newlines=True)
247
248
218def apt_hold(packages, fatal=False):249def apt_hold(packages, fatal=False):
219 """Hold one or more packages"""250 return apt_mark(packages, 'hold', fatal=fatal)
220 cmd = ['apt-mark', 'hold']251
221 if isinstance(packages, six.string_types):252
222 cmd.append(packages)253def apt_unhold(packages, fatal=False):
223 else:254 return apt_mark(packages, 'unhold', fatal=fatal)
224 cmd.extend(packages)
225 log("Holding {}".format(packages))
226
227 if fatal:
228 subprocess.check_call(cmd)
229 else:
230 subprocess.call(cmd)
231255
232256
233def add_source(source, key=None):257def add_source(source, key=None):
@@ -370,8 +394,9 @@
370 for handler in handlers:394 for handler in handlers:
371 try:395 try:
372 installed_to = handler.install(source, *args, **kwargs)396 installed_to = handler.install(source, *args, **kwargs)
373 except UnhandledSource:397 except UnhandledSource as e:
374 pass398 log('Install source attempt unsuccessful: {}'.format(e),
399 level='WARNING')
375 if not installed_to:400 if not installed_to:
376 raise UnhandledSource("No handler found for source {}".format(source))401 raise UnhandledSource("No handler found for source {}".format(source))
377 return installed_to402 return installed_to
@@ -394,7 +419,7 @@
394 importlib.import_module(package),419 importlib.import_module(package),
395 classname)420 classname)
396 plugin_list.append(handler_class())421 plugin_list.append(handler_class())
397 except (ImportError, AttributeError):422 except NotImplementedError:
398 # Skip missing plugins so that they can be ommitted from423 # Skip missing plugins so that they can be ommitted from
399 # installation if desired424 # installation if desired
400 log("FetchHandler {} not found, skipping plugin".format(425 log("FetchHandler {} not found, skipping plugin".format(
401426
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2016-04-05 14:45:53 +0000
@@ -18,6 +18,16 @@
18import hashlib18import hashlib
19import re19import re
2020
21from charmhelpers.fetch import (
22 BaseFetchHandler,
23 UnhandledSource
24)
25from charmhelpers.payload.archive import (
26 get_archive_handler,
27 extract,
28)
29from charmhelpers.core.host import mkdir, check_hash
30
21import six31import six
22if six.PY3:32if six.PY3:
23 from urllib.request import (33 from urllib.request import (
@@ -35,16 +45,6 @@
35 )45 )
36 from urlparse import urlparse, urlunparse, parse_qs46 from urlparse import urlparse, urlunparse, parse_qs
3747
38from charmhelpers.fetch import (
39 BaseFetchHandler,
40 UnhandledSource
41)
42from charmhelpers.payload.archive import (
43 get_archive_handler,
44 extract,
45)
46from charmhelpers.core.host import mkdir, check_hash
47
4848
49def splituser(host):49def splituser(host):
50 '''urllib.splituser(), but six's support of this seems broken'''50 '''urllib.splituser(), but six's support of this seems broken'''
@@ -77,6 +77,8 @@
77 def can_handle(self, source):77 def can_handle(self, source):
78 url_parts = self.parse_url(source)78 url_parts = self.parse_url(source)
79 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):79 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
80 # XXX: Why is this returning a boolean and a string? It's
81 # doomed to fail since "bool(can_handle('foo://'))" will be True.
80 return "Wrong source type"82 return "Wrong source type"
81 if get_archive_handler(self.base_url(source)):83 if get_archive_handler(self.base_url(source)):
82 return True84 return True
@@ -106,7 +108,7 @@
106 install_opener(opener)108 install_opener(opener)
107 response = urlopen(source)109 response = urlopen(source)
108 try:110 try:
109 with open(dest, 'w') as dest_file:111 with open(dest, 'wb') as dest_file:
110 dest_file.write(response.read())112 dest_file.write(response.read())
111 except Exception as e:113 except Exception as e:
112 if os.path.isfile(dest):114 if os.path.isfile(dest):
@@ -155,7 +157,11 @@
155 else:157 else:
156 algorithms = hashlib.algorithms_available158 algorithms = hashlib.algorithms_available
157 if key in algorithms:159 if key in algorithms:
158 check_hash(dld_file, value, key)160 if len(value) != 1:
161 raise TypeError(
162 "Expected 1 hash value, not %d" % len(value))
163 expected = value[0]
164 check_hash(dld_file, expected, key)
159 if checksum:165 if checksum:
160 check_hash(dld_file, checksum, hash_type)166 check_hash(dld_file, checksum, hash_type)
161 return extract(dld_file, dest)167 return extract(dld_file, dest)
162168
=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/fetch/bzrurl.py 2016-04-05 14:45:53 +0000
@@ -15,60 +15,54 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from subprocess import check_call, CalledProcessError
18from charmhelpers.fetch import (19from charmhelpers.fetch import (
19 BaseFetchHandler,20 BaseFetchHandler,
20 UnhandledSource21 UnhandledSource,
22 filter_installed_packages,
23 apt_install,
21)24)
22from charmhelpers.core.host import mkdir25from charmhelpers.core.host import mkdir
2326
24import six
25if six.PY3:
26 raise ImportError('bzrlib does not support Python3')
2727
28try:28if filter_installed_packages(['bzr']) != []:
29 from bzrlib.branch import Branch29 apt_install(['bzr'])
30 from bzrlib import bzrdir, workingtree, errors30 if filter_installed_packages(['bzr']) != []:
31except ImportError:31 raise NotImplementedError('Unable to install bzr')
32 from charmhelpers.fetch import apt_install
33 apt_install("python-bzrlib")
34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
3632
3733
38class BzrUrlFetchHandler(BaseFetchHandler):34class BzrUrlFetchHandler(BaseFetchHandler):
39 """Handler for bazaar branches via generic and lp URLs"""35 """Handler for bazaar branches via generic and lp URLs"""
40 def can_handle(self, source):36 def can_handle(self, source):
41 url_parts = self.parse_url(source)37 url_parts = self.parse_url(source)
42 if url_parts.scheme not in ('bzr+ssh', 'lp'):38 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
43 return False39 return False
40 elif not url_parts.scheme:
41 return os.path.exists(os.path.join(source, '.bzr'))
44 else:42 else:
45 return True43 return True
4644
47 def branch(self, source, dest):45 def branch(self, source, dest):
48 url_parts = self.parse_url(source)
49 # If we use lp:branchname scheme we need to load plugins
50 if not self.can_handle(source):46 if not self.can_handle(source):
51 raise UnhandledSource("Cannot handle {}".format(source))47 raise UnhandledSource("Cannot handle {}".format(source))
52 if url_parts.scheme == "lp":48
53 from bzrlib.plugin import load_plugins49 # if the target is a bzr repo, pull, else branch
54 load_plugins()50 try:
55 try:51 check_call(['bzr', 'revno', dest])
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)52 except CalledProcessError:
57 except errors.AlreadyControlDirError:53 check_call(['bzr', 'branch', '--use-existing-dir', source, dest])
58 local_branch = Branch.open(dest)54 else:
59 try:55 check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
60 remote_branch = Branch.open(source)56
61 remote_branch.push(local_branch)57 def install(self, source, dest=None):
62 tree = workingtree.WorkingTree.open(dest)
63 tree.update()
64 except Exception as e:
65 raise e
66
67 def install(self, source):
68 url_parts = self.parse_url(source)58 url_parts = self.parse_url(source)
69 branch_name = url_parts.path.strip("/").split("/")[-1]59 branch_name = url_parts.path.strip("/").split("/")[-1]
70 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",60 if dest:
71 branch_name)61 dest_dir = os.path.join(dest, branch_name)
62 else:
63 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
64 branch_name)
65
72 if not os.path.exists(dest_dir):66 if not os.path.exists(dest_dir):
73 mkdir(dest_dir, perms=0o755)67 mkdir(dest_dir, perms=0o755)
74 try:68 try:
7569
=== modified file 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2015-01-27 14:54:02 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2016-04-05 14:45:53 +0000
@@ -15,24 +15,18 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from subprocess import check_call, CalledProcessError
18from charmhelpers.fetch import (19from charmhelpers.fetch import (
19 BaseFetchHandler,20 BaseFetchHandler,
20 UnhandledSource21 UnhandledSource,
22 filter_installed_packages,
23 apt_install,
21)24)
22from charmhelpers.core.host import mkdir25
2326if filter_installed_packages(['git']) != []:
24import six27 apt_install(['git'])
25if six.PY3:28 if filter_installed_packages(['git']) != []:
26 raise ImportError('GitPython does not support Python 3')29 raise NotImplementedError('Unable to install git')
27
28try:
29 from git import Repo
30except ImportError:
31 from charmhelpers.fetch import apt_install
32 apt_install("python-git")
33 from git import Repo
34
35from git.exc import GitCommandError
3630
3731
38class GitUrlFetchHandler(BaseFetchHandler):32class GitUrlFetchHandler(BaseFetchHandler):
@@ -40,19 +34,26 @@
40 def can_handle(self, source):34 def can_handle(self, source):
41 url_parts = self.parse_url(source)35 url_parts = self.parse_url(source)
42 # TODO (mattyw) no support for ssh git@ yet36 # TODO (mattyw) no support for ssh git@ yet
43 if url_parts.scheme not in ('http', 'https', 'git'):37 if url_parts.scheme not in ('http', 'https', 'git', ''):
44 return False38 return False
39 elif not url_parts.scheme:
40 return os.path.exists(os.path.join(source, '.git'))
45 else:41 else:
46 return True42 return True
4743
48 def clone(self, source, dest, branch):44 def clone(self, source, dest, branch="master", depth=None):
49 if not self.can_handle(source):45 if not self.can_handle(source):
50 raise UnhandledSource("Cannot handle {}".format(source))46 raise UnhandledSource("Cannot handle {}".format(source))
5147
52 repo = Repo.clone_from(source, dest)48 if os.path.exists(dest):
53 repo.git.checkout(branch)49 cmd = ['git', '-C', dest, 'pull', source, branch]
50 else:
51 cmd = ['git', 'clone', source, dest, '--branch', branch]
52 if depth:
53 cmd.extend(['--depth', depth])
54 check_call(cmd)
5455
55 def install(self, source, branch="master", dest=None):56 def install(self, source, branch="master", dest=None, depth=None):
56 url_parts = self.parse_url(source)57 url_parts = self.parse_url(source)
57 branch_name = url_parts.path.strip("/").split("/")[-1]58 branch_name = url_parts.path.strip("/").split("/")[-1]
58 if dest:59 if dest:
@@ -60,12 +61,10 @@
60 else:61 else:
61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",62 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
62 branch_name)63 branch_name)
63 if not os.path.exists(dest_dir):
64 mkdir(dest_dir, perms=0o755)
65 try:64 try:
66 self.clone(source, dest_dir, branch)65 self.clone(source, dest_dir, branch, depth)
67 except GitCommandError as e:66 except CalledProcessError as e:
68 raise UnhandledSource(e.message)67 raise UnhandledSource(e)
69 except OSError as e:68 except OSError as e:
70 raise UnhandledSource(e.strerror)69 raise UnhandledSource(e.strerror)
71 return dest_dir70 return dest_dir
7271
=== added directory 'hooks/diverted'
=== renamed symlink 'hooks/install' => 'hooks/diverted/install'
=== target changed u'hooks.py' => u'../hooks.py'
=== added file 'hooks/install'
--- hooks/install 1970-01-01 00:00:00 +0000
+++ hooks/install 2016-04-05 14:45:53 +0000
@@ -0,0 +1,7 @@
1#!/bin/bash
2# Wrapper to deal with newer Ubuntu versions that don't have py2 installed
3# by default.
4
5apt-get install -y python python-apt python-requests python-six python-yaml
6
7exec ./hooks/diverted/install
08
=== modified file 'hooks/services.py'
--- hooks/services.py 2015-01-30 10:22:34 +0000
+++ hooks/services.py 2016-04-05 14:45:53 +0000
@@ -1,4 +1,5 @@
1import os1import os
2from urlparse import urlparse
23
3from charmhelpers.core import hookenv4from charmhelpers.core import hookenv
4from charmhelpers.core.services.base import ServiceManager5from charmhelpers.core.services.base import ServiceManager
@@ -15,6 +16,9 @@
15 'service': 'wp-db',16 'service': 'wp-db',
16 'required_data': [17 'required_data': [
17 helpers.MysqlRelation(),18 helpers.MysqlRelation(),
19 {
20 'outbound_http_proxy': urlparse(config['outbound_http_proxy'])
21 },
18 helpers.StoredContext('wp-secrets.json',22 helpers.StoredContext('wp-secrets.json',
19 actions.generate_secrets()),23 actions.generate_secrets()),
20 ],24 ],
@@ -61,7 +65,7 @@
61 helpers.MysqlRelation(),65 helpers.MysqlRelation(),
62 wp_helpers.wordpress_configured(),66 wp_helpers.wordpress_configured(),
63 helpers.RequiredConfig('akismet_key'),67 helpers.RequiredConfig('akismet_key'),
64 ],68 ],
65 'data_ready': [actions.enable_akismet],69 'data_ready': [actions.enable_akismet],
66 },70 },
67 {71 {
@@ -102,34 +106,10 @@
102 helpers.RequiredConfig(),106 helpers.RequiredConfig(),
103 ],107 ],
104 'data_ready': [108 'data_ready': [
105 helpers.render_template(109 actions.write_nrpe_checks
106 source='wp-nrpe.j2',
107 target='/etc/nagios/nrpe.d/check_{}.cfg'.format(
108 hookenv.local_unit().replace('/', '-'),
109 )
110 ),
111 helpers.render_template(
112 source='wp-nagios.j2',
113 target='/var/lib/nagios/export/service__{}-{}.cfg'.format(
114 config['nagios_context'],
115 hookenv.local_unit().replace('/', '-'),
116 )
117 ),
118 ],110 ],
119 'data_lost': [111 'data_lost': [
120 helpers.render_template(112 actions.wipe_nrpe_checks
121 source='wp-nrpe.j2',
122 target='/etc/nagios/nrpe.d/check_{}.cfg'.format(
123 hookenv.local_unit().replace('/', '-'),
124 )
125 ),
126 helpers.render_template(
127 source='wp-nagios.j2',
128 target='/var/lib/nagios/export/service__{}-{}.cfg'.format(
129 config['nagios_context'],
130 hookenv.local_unit().replace('/', '-'),
131 )
132 ),
133 ],113 ],
134 },114 },
135 ])115 ])
136116
=== modified file 'hooks/wp_helpers.py'
--- hooks/wp_helpers.py 2015-02-25 17:05:27 +0000
+++ hooks/wp_helpers.py 2016-04-05 14:45:53 +0000
@@ -1,5 +1,6 @@
1import os1import os
2import re2import re
3import yaml
3import requests4import requests
4import urlparse5import urlparse
56
@@ -36,6 +37,14 @@
36 return []37 return []
3738
3839
40def get_vhost_options():
41 config = hookenv.config()
42 if config.get('vhost_options', False):
43 vhost_options = yaml.safe_load(config['vhost_options'])
44 return vhost_options
45 return None
46
47
39class PluginRelation(helpers.RelationContext):48class PluginRelation(helpers.RelationContext):
40 name = 'wordpress-plugin'49 name = 'wordpress-plugin'
41 interface = 'wordpress-plugin'50 interface = 'wordpress-plugin'
@@ -85,7 +94,9 @@
85 urls = []94 urls = []
86 hostnames = [config["blog_hostname"]]95 hostnames = [config["blog_hostname"]]
87 if config["additional_hostnames"]:96 if config["additional_hostnames"]:
88 hostnames.extend([h.strip() for h in config["additional_hostnames"].split(",")])97 hostnames.extend(
98 [h.strip() for h in
99 config["additional_hostnames"].split(",")])
89 for hostname in hostnames:100 for hostname in hostnames:
90 urls.append(urlparse.ParseResult(101 urls.append(urlparse.ParseResult(
91 'http',102 'http',
@@ -102,11 +113,12 @@
102 config = hookenv.config()113 config = hookenv.config()
103 services = []114 services = []
104 redirects = get_redirects()115 redirects = get_redirects()
116 extra_vhost_options = get_vhost_options()
105 proxy = self.get_proxy()117 proxy = self.get_proxy()
106 base_vhost = {"type": "php",118 base_vhost = {"type": "php",
107 "document_root": str(config["install_path"]),119 "document_root": str(config["install_path"]),
108 "webserver_options": ["mod_rewrite", "mod_headers"],120 "webserver_options": ["mod_rewrite", "mod_headers"],
109 "vhost_options": {'Header': 'append Vary "Cookie"'},121 "vhost_options": [{'Header': 'append Vary "Cookie"'}],
110 }122 }
111 for url in self.get_urls():123 for url in self.get_urls():
112 vhost = base_vhost.copy()124 vhost = base_vhost.copy()
@@ -115,6 +127,10 @@
115 vhost["proxy"] = proxy127 vhost["proxy"] = proxy
116 if url.scheme == "http" and redirects:128 if url.scheme == "http" and redirects:
117 vhost["redirects"] = redirects129 vhost["redirects"] = redirects
130 if "redirects" in config and config["redirects"]:
131 vhost["redirect_match"] = yaml.safe_load(config["redirects"])
132 if extra_vhost_options:
133 vhost["vhost_options"] = vhost["vhost_options"] + extra_vhost_options
118 services.append(vhost)134 services.append(vhost)
119135
120 return {'services': services}136 return {'services': services}
@@ -177,8 +193,8 @@
177 'ignore-must-revalidate',193 'ignore-must-revalidate',
178 'ignore-private',194 'ignore-private',
179 'ignore-auth'195 'ignore-auth'
180 ]196 ]
181 }]197 }]
182198
183 new_services = []199 new_services = []
184 for service in services:200 for service in services:
@@ -196,6 +212,8 @@
196 del(service['document_root'])212 del(service['document_root'])
197 if 'webserver_options' in service:213 if 'webserver_options' in service:
198 del(service['webserver_options'])214 del(service['webserver_options'])
215 if 'redirect_match' in service:
216 del(service['redirect_match'])
199 new_services.append(service)217 new_services.append(service)
200 return {'services': new_services}218 return {'services': new_services}
201219
202220
=== modified file 'metadata.yaml'
--- metadata.yaml 2015-02-24 17:05:07 +0000
+++ metadata.yaml 2016-04-05 14:45:53 +0000
@@ -1,5 +1,6 @@
1name: wordpress1name: wordpress
2maintainer: Nick Moffit <nick.moffit@canonical.com> Jacek Nykis <jacek.nykis@canonical.com>2maintainer: Nick Moffit <nick.moffit@canonical.com>
3maintainer: Jacek Nykis <jacek.nykis@canonical.com>
3summary: "Wordpress"4summary: "Wordpress"
4description: "Wordpress blog"5description: "Wordpress blog"
5categories: ["applications"]6categories: ["applications"]
67
=== modified file 'templates/wp-apparmor.j2'
--- templates/wp-apparmor.j2 2014-12-11 18:10:02 +0000
+++ templates/wp-apparmor.j2 2016-04-05 14:45:53 +0000
@@ -15,6 +15,7 @@
15 # Webserver subordinate will drop relevant rules here15 # Webserver subordinate will drop relevant rules here
16 #include <webserver.d>16 #include <webserver.d>
1717
18 @{PROC}/@{pid}/** r,
18 {{install_path}}/ r,19 {{install_path}}/ r,
19 {{install_path}}/** r,20 {{install_path}}/** r,
20 {{install_path}}/wp-content/uploads/ rw,21 {{install_path}}/wp-content/uploads/ rw,
2122
=== modified file 'templates/wp-info.php.j2'
--- templates/wp-info.php.j2 2015-01-29 15:48:37 +0000
+++ templates/wp-info.php.j2 2016-04-05 14:45:53 +0000
@@ -51,3 +51,16 @@
5151
52$table_prefix = 'wp_';52$table_prefix = 'wp_';
5353
54{% if outbound_http_proxy.hostname and outbound_http_proxy.port %}
55define('WP_PROXY_HOST', '{{outbound_http_proxy.hostname}}');
56define('WP_PROXY_PORT', '{{outbound_http_proxy.port}}');
57define('WP_PROXY_BYPASS_HOSTS', 'localhost');
58{% endif %}
59
60{% if outbound_http_proxy.username %}
61define('WP_PROXY_USERNAME', '{{outbound_http_proxy.username}}');
62{% endif %}
63
64{% if outbound_http_proxy.username and outbound_http_proxy.password %}
65define('WP_PROXY_PASSWORD', '{{outbound_http_proxy.password}}');
66{% endif %}
5467
=== removed file 'templates/wp-nagios.j2'
--- templates/wp-nagios.j2 2014-12-17 11:21:05 +0000
+++ templates/wp-nagios.j2 1970-01-01 00:00:00 +0000
@@ -1,48 +0,0 @@
1#
2# " "
3# mmm m m mmm m m
4# # # # # # #
5# # # # # # #
6# # "mm"# # "mm"#
7# # #
8# "" ""
9# This file is managed by Juju. Do not make local changes.
10
11{% macro servicegroup(default='juju') -%}
12 {%- if config['nagios_servicegroups'] -%}
13{{ config['nagios_servicegroups'] }}
14 {%- elif config['nagios_context'] -%}
15{{ config['nagios_context'] }}
16 {%- else -%}
17{{ default }}
18 {%- endif -%}
19{%- endmacro %}
20
21{% for rel in nrpe_external_master -%}
22define service {
23 use active-service
24 host_name {{rel.nagios_hostname}}
25 service_description {{rel.nagios_hostname}}[wordpress_http] Check Wordpress HTTP
26 check_command check_nrpe!check_wordpress_http
27 servicegroups {{ servicegroup() }}
28}
29
30 {% if config['ssl_enabled'] -%}
31define service {
32 use active-service
33 host_name {{rel.nagios_hostname}}
34 service_description {{rel.nagios_hostname}}[wordpress_https] Check Wordpress HTTPS
35 check_command check_nrpe!check_wordpress_https
36 servicegroups {{ servicegroup() }}
37}
38 {%- endif %}
39 {% for plugin in wordpress_plugin %}
40define service {
41 use active-service
42 host_name {{rel.nagios_hostname}}
43 service_description {{rel.nagios_hostname}}[{{plugin['plugin_name']|replace('-','_')}}] Check {{plugin['plugin_name']}} Wordpress Plug-In
44 check_command check_nrpe!check_{{plugin['plugin_name']|replace('-','_')}}
45 servicegroups {{ servicegroup() }}
46}
47 {%- endfor %}
48{%- endfor %}
490
=== removed file 'templates/wp-nrpe.j2'
--- templates/wp-nrpe.j2 2014-12-22 10:40:03 +0000
+++ templates/wp-nrpe.j2 1970-01-01 00:00:00 +0000
@@ -1,17 +0,0 @@
1#
2# " "
3# mmm m m mmm m m
4# # # # # # #
5# # # # # # #
6# # "mm"# # "mm"#
7# # #
8# "" ""
9# This file is managed by Juju. Do not make local changes.
10
11command[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 %}
12{% if config['ssl_enabled'] -%}
13command[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 %}
14{%- endif %}
15{%- for rel in wordpress_plugin %}
16command[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']}}
17{% endfor -%}

Subscribers

People subscribed via source and target branches