Merge lp:~opnfv-team/charms/trusty/percona-cluster/power8 into lp:charms/trusty/percona-cluster

Proposed by Narinder Gupta
Status: Needs review
Proposed branch: lp:~opnfv-team/charms/trusty/percona-cluster/power8
Merge into: lp:charms/trusty/percona-cluster
Diff against target: 14145 lines (+13604/-0) (has conflicts)
89 files modified
.coveragerc (+6/-0)
.gitignore (+10/-0)
.gitreview (+4/-0)
.testr.conf (+8/-0)
Makefile (+29/-0)
README.md (+71/-0)
actions.yaml (+20/-0)
actions/actions.py (+99/-0)
charm-helpers-hooks.yaml (+12/-0)
charm-helpers-tests.yaml (+6/-0)
charmhelpers/__init__.py (+38/-0)
charmhelpers/cli/__init__.py (+191/-0)
charmhelpers/cli/benchmark.py (+36/-0)
charmhelpers/cli/commands.py (+32/-0)
charmhelpers/cli/hookenv.py (+23/-0)
charmhelpers/cli/host.py (+31/-0)
charmhelpers/cli/unitdata.py (+39/-0)
charmhelpers/contrib/__init__.py (+15/-0)
charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
charmhelpers/contrib/charmsupport/nrpe.py (+398/-0)
charmhelpers/contrib/charmsupport/volumes.py (+175/-0)
charmhelpers/contrib/database/mysql.py (+415/-0)
charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
charmhelpers/contrib/hahelpers/cluster.py (+316/-0)
charmhelpers/contrib/network/__init__.py (+15/-0)
charmhelpers/contrib/network/ip.py (+458/-0)
charmhelpers/contrib/peerstorage/__init__.py (+269/-0)
charmhelpers/core/__init__.py (+15/-0)
charmhelpers/core/decorators.py (+57/-0)
charmhelpers/core/files.py (+45/-0)
charmhelpers/core/fstab.py (+134/-0)
charmhelpers/core/hookenv.py (+978/-0)
charmhelpers/core/host.py (+659/-0)
charmhelpers/core/hugepage.py (+71/-0)
charmhelpers/core/kernel.py (+68/-0)
charmhelpers/core/services/__init__.py (+18/-0)
charmhelpers/core/services/base.py (+353/-0)
charmhelpers/core/services/helpers.py (+292/-0)
charmhelpers/core/strutils.py (+72/-0)
charmhelpers/core/sysctl.py (+56/-0)
charmhelpers/core/templating.py (+81/-0)
charmhelpers/core/unitdata.py (+521/-0)
charmhelpers/fetch/__init__.py (+464/-0)
charmhelpers/fetch/archiveurl.py (+167/-0)
charmhelpers/fetch/bzrurl.py (+68/-0)
charmhelpers/fetch/giturl.py (+68/-0)
charmhelpers/payload/__init__.py (+17/-0)
charmhelpers/payload/execd.py (+66/-0)
config.yaml (+154/-0)
copyright (+29/-0)
hooks/install (+20/-0)
hooks/percona_hooks.py (+644/-0)
hooks/percona_utils.py (+419/-0)
keys/repo.percona.com (+30/-0)
metadata.yaml (+26/-0)
ocf/percona/mysql_monitor (+636/-0)
requirements.txt (+12/-0)
revision (+1/-0)
setup.cfg (+6/-0)
templates/my.cnf (+90/-0)
templates/mysqld.cnf (+116/-0)
test-requirements.txt (+8/-0)
tests/10-deploy_test.py (+29/-0)
tests/20-broken-mysqld.py (+38/-0)
tests/30-kill-9-mysqld.py (+38/-0)
tests/31-test-pause-and-resume.py (+38/-0)
tests/40-test-bootstrap-single.py (+17/-0)
tests/41-test-bootstrap-multi-notmin.py (+41/-0)
tests/42-test-bootstrap-multi-min.py (+43/-0)
tests/README (+113/-0)
tests/basic_deployment.py (+193/-0)
tests/charmhelpers/__init__.py (+38/-0)
tests/charmhelpers/contrib/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+95/-0)
tests/charmhelpers/contrib/amulet/utils.py (+818/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+301/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+985/-0)
tests/charmhelpers/core/__init__.py (+15/-0)
tests/charmhelpers/core/hookenv.py (+978/-0)
tests/setup/00-setup (+18/-0)
tests/tests.yaml (+21/-0)
tox.ini (+30/-0)
unit_tests/__init__.py (+3/-0)
unit_tests/test_percona_hooks.py (+131/-0)
unit_tests/test_percona_utils.py (+233/-0)
unit_tests/test_utils.py (+121/-0)
Conflict adding file .coveragerc.  Moved existing file to .coveragerc.moved.
Conflict adding file .gitignore.  Moved existing file to .gitignore.moved.
Conflict adding file .gitreview.  Moved existing file to .gitreview.moved.
Conflict adding file Makefile.  Moved existing file to Makefile.moved.
Conflict adding file README.md.  Moved existing file to README.md.moved.
Conflict adding file actions.  Moved existing file to actions.moved.
Conflict adding file actions.yaml.  Moved existing file to actions.yaml.moved.
Conflict adding file charm-helpers-hooks.yaml.  Moved existing file to charm-helpers-hooks.yaml.moved.
Conflict adding file charm-helpers-tests.yaml.  Moved existing file to charm-helpers-tests.yaml.moved.
Conflict adding file charmhelpers.  Moved existing file to charmhelpers.moved.
Conflict adding file config.yaml.  Moved existing file to config.yaml.moved.
Conflict adding file copyright.  Moved existing file to copyright.moved.
Conflict adding file hooks.  Moved existing file to hooks.moved.
Conflict adding file keys.  Moved existing file to keys.moved.
Conflict adding file metadata.yaml.  Moved existing file to metadata.yaml.moved.
Conflict adding file ocf.  Moved existing file to ocf.moved.
Conflict adding file revision.  Moved existing file to revision.moved.
Conflict adding file setup.cfg.  Moved existing file to setup.cfg.moved.
Conflict adding file templates.  Moved existing file to templates.moved.
Conflict adding file tests.  Moved existing file to tests.moved.
Conflict adding file unit_tests.  Moved existing file to unit_tests.moved.
To merge this branch: bzr merge lp:~opnfv-team/charms/trusty/percona-cluster/power8
Reviewer Review Type Date Requested Status
David Ames Pending
OpenStack Charmers Pending
Review via email: mp+288722@code.launchpad.net

Description of the change

this merge proposal is from IBM where percona-cluster charm was failing to install on power8 system. It seems there was no root-password was set during install and even config option did not help. But after doing this modification we are able to install this on Power8 system.

To post a comment you must log in.

Unmerged revisions

90. By Narinder Gupta

modified to include the power8 changes.

89. By Jill Rouleau

Add backup action

Add a new action to backup the pxc database on a unit.

Backups by default are stored in /opt/backups/mysql and can
optionally be compressed and done as incremental backups.

Change-Id: I5c6ab9fd8be7cb6cdb2a26e849ec0b22d8d4f9a6

88. By James Page

Add tox support for check/gate

This charm was missed pre-migration to git/gerrit.

Add support for executing pep8 and unit tests using tox.

Change-Id: I5518e870c572ccc292d6fe4e9b7c910c7f3f0260

87. By uosci-testing-bot

Adapt imports and metadata for github move

86. By James Page

Add gitreview prior to migration to openstack

85. By Liam Young

[thedac, r=gnuoy] Charm helpers sync. Make the service_running status check cover percona-cluster's unique status message.

84. By David Ames

[jamespage, r=thedac] Fix Bug:1481362 Make datadir dynamic depending on ubuntu version

83. By Liam Young

Update test combo definitions, remove Vivid deprecated release tests, update bundletester testplan yaml, update tests README.

82. By Liam Young

[hopem, r=gnuoy]
Improve shared-db rel logging and tidy get_db_host
Closes-Bug: 1512293

81. By James Page

Update maintainer

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file '.coveragerc'
--- .coveragerc 1970-01-01 00:00:00 +0000
+++ .coveragerc 2016-03-10 22:55:31 +0000
@@ -0,0 +1,6 @@
1[report]
2# Regexes for lines to exclude from consideration
3exclude_lines =
4 if __name__ == .__main__.:
5include=
6 hooks/percona*
07
=== renamed file '.coveragerc' => '.coveragerc.moved'
=== added file '.gitignore'
--- .gitignore 1970-01-01 00:00:00 +0000
+++ .gitignore 2016-03-10 22:55:31 +0000
@@ -0,0 +1,10 @@
1bin
2.coverage
3.pydevproject
4.project
5*.pyc
6*.pyo
7__pycache__
8*.sw[nop]
9.testrepository
10.tox
011
=== renamed file '.gitignore' => '.gitignore.moved'
=== added file '.gitreview'
--- .gitreview 1970-01-01 00:00:00 +0000
+++ .gitreview 2016-03-10 22:55:31 +0000
@@ -0,0 +1,4 @@
1[gerrit]
2host=review.openstack.org
3port=29418
4project=openstack/charm-percona-cluster.git
05
=== renamed file '.gitreview' => '.gitreview.moved'
=== added file '.testr.conf'
--- .testr.conf 1970-01-01 00:00:00 +0000
+++ .testr.conf 2016-03-10 22:55:31 +0000
@@ -0,0 +1,8 @@
1[DEFAULT]
2test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
3 OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
4 OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
5 ${PYTHON:-python} -m subunit.run discover -t ./ ./unit_tests $LISTOPT $IDOPTION
6
7test_id_option=--load-list $IDFILE
8test_list_option=--list
09
=== added file 'Makefile'
--- Makefile 1970-01-01 00:00:00 +0000
+++ Makefile 2016-03-10 22:55:31 +0000
@@ -0,0 +1,29 @@
1#!/usr/bin/make
2PYTHON := /usr/bin/env python
3export PYTHONPATH := hooks
4
5lint:
6 @flake8 --exclude hooks/charmhelpers,tests/charmhelpers \
7 actions hooks unit_tests tests
8 @charm proof
9
10test:
11 @# Bundletester expects unit tests here.
12 @$(PYTHON) /usr/bin/nosetests -v --nologcapture --with-coverage unit_tests
13
14functional_test:
15 @echo Starting amulet tests...
16 @tests/setup/00-setup
17 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
18
19bin/charm_helpers_sync.py:
20 @mkdir -p bin
21 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
22 > bin/charm_helpers_sync.py
23
24sync: bin/charm_helpers_sync.py
25 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
26 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
27
28publish: lint test
29 bzr push lp:charms/trusty/percona-cluster
030
=== renamed file 'Makefile' => 'Makefile.moved'
=== added file 'README.md'
--- README.md 1970-01-01 00:00:00 +0000
+++ README.md 2016-03-10 22:55:31 +0000
@@ -0,0 +1,71 @@
1Overview
2========
3
4Percona XtraDB Cluster is a high availability and high scalability solution for
5MySQL clustering. Percona XtraDB Cluster integrates Percona Server with the
6Galera library of MySQL high availability solutions in a single product package
7which enables you to create a cost-effective MySQL cluster.
8
9This charm deploys Percona XtraDB Cluster onto Ubuntu.
10
11Usage
12=====
13
14WARNING: Its critical that you follow the bootstrap process detailed in this
15document in order to end up with a running Active/Active Percona Cluster.
16
17Proxy Configuration
18-------------------
19
20If you are deploying this charm on MAAS or in an environment without direct
21access to the internet, you will need to allow access to repo.percona.com
22as the charm installs packages direct from the Percona respositories. If you
23are using squid-deb-proxy, follow the steps below:
24
25 echo "repo.percona.com" | sudo tee /etc/squid-deb-proxy/mirror-dstdomain.acl.d/40-percona
26 sudo service squid-deb-proxy restart
27
28Deployment
29----------
30
31The first service unit deployed acts as the seed node for the rest of the
32cluster; in order for the cluster to function correctly, the same MySQL passwords
33must be used across all nodes:
34
35 cat > percona.yaml << EOF
36 percona-cluster:
37 root-password: my-root-password
38 sst-password: my-sst-password
39 EOF
40
41Once you have created this file, you can deploy the first seed unit:
42
43 juju deploy --config percona.yaml percona-cluster
44
45Once this node is full operational, you can add extra units one at a time to the
46deployment:
47
48 juju add-unit percona-cluster
49
50A minimium cluster size of three units is recommended.
51
52In order to access the cluster, use the hacluster charm to provide a single IP
53address:
54
55 juju set percona-cluster vip=10.0.3.200
56 juju deploy hacluster
57 juju add-relation hacluster percona-cluster
58
59Clients can then access using the vip provided. This vip will be passed to
60related services:
61
62 juju add-relation keystone percona-cluster
63
64
65Limitiations
66============
67
68Note that Percona XtraDB Cluster is not a 'scale-out' MySQL solution; reads
69and writes are channelled through a single service unit and synchronously
70replicated to other nodes in the cluster; reads/writes are as slow as the
71slowest node you have in your deployment.
072
=== renamed file 'README.md' => 'README.md.moved'
=== added directory 'actions'
=== renamed directory 'actions' => 'actions.moved'
=== added file 'actions.yaml'
--- actions.yaml 1970-01-01 00:00:00 +0000
+++ actions.yaml 2016-03-10 22:55:31 +0000
@@ -0,0 +1,20 @@
1pause:
2 description: Pause the MySQL service.
3resume:
4 description: Resume the MySQL service.
5backup:
6 description: Full database backup
7 params:
8 basedir:
9 type: string
10 default: "/opt/backups/mysql"
11 description: The base directory for backups
12 compress:
13 type: boolean
14 default: false
15 description: Whether or not to compress the backup
16 incremental:
17 type: boolean
18 default: false
19 description: Make an incremental database backup
20
021
=== renamed file 'actions.yaml' => 'actions.yaml.moved'
=== added file 'actions/actions.py'
--- actions/actions.py 1970-01-01 00:00:00 +0000
+++ actions/actions.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,99 @@
1#!/usr/bin/python
2
3import os
4import sys
5import subprocess
6import traceback
7from time import gmtime, strftime
8
9from charmhelpers.core.host import service_pause, service_resume
10from charmhelpers.core.hookenv import (
11 action_get,
12 action_set,
13 action_fail,
14 status_set,
15 config,
16)
17
18from percona_utils import assess_status
19
20MYSQL_SERVICE = "mysql"
21
22
23def pause(args):
24 """Pause the MySQL service.
25
26 @raises Exception should the service fail to stop.
27 """
28 if not service_pause(MYSQL_SERVICE):
29 raise Exception("Failed to pause MySQL service.")
30 status_set(
31 "maintenance",
32 "Unit paused - use 'resume' action to resume normal service")
33
34
35def resume(args):
36 """Resume the MySQL service.
37
38 @raises Exception should the service fail to start."""
39 if not service_resume(MYSQL_SERVICE):
40 raise Exception("Failed to resume MySQL service.")
41 assess_status()
42
43
44def backup():
45 basedir = (action_get("basedir")).lower()
46 compress = (action_get("compress"))
47 incremental = (action_get("incremental"))
48 sstpw = config("sst-password")
49 optionlist = []
50
51 # innobackupex will not create recursive dirs that do not already exist,
52 # so help it along
53 if not os.path.exists(basedir):
54 os.makedirs(basedir)
55
56 # Build a list of options to pass to innobackupex
57 if compress is "true":
58 optionlist.append("--compress")
59
60 if incremental is "true":
61 optionlist.append("--incremental")
62
63 try:
64 subprocess.check_call(
65 ['innobackupex', '--compact', '--galera-info', '--rsync',
66 basedir, '--user=sstuser', '--password=' + sstpw] + optionlist)
67 action_set({
68 'time-completed': (strftime("%Y-%m-%d %H:%M:%S", gmtime())),
69 'outcome': 'Success'}
70 )
71 except subprocess.CalledProcessError as e:
72 action_set({
73 'time-completed': (strftime("%Y-%m-%d %H:%M:%S", gmtime())),
74 'output': e.output,
75 'return-code': e.returncode,
76 'traceback': traceback.format_exc()})
77 action_fail("innobackupex failed, you should log on to the unit"
78 "and check the status of the database")
79
80# A dictionary of all the defined actions to callables (which take
81# parsed arguments).
82ACTIONS = {"pause": pause, "resume": resume, "backup": backup}
83
84
85def main(args):
86 action_name = os.path.basename(args[0])
87 try:
88 action = ACTIONS[action_name]
89 except KeyError:
90 return "Action %s undefined" % action_name
91 else:
92 try:
93 action(args)
94 except Exception as e:
95 action_fail(str(e))
96
97
98if __name__ == "__main__":
99 sys.exit(main(sys.argv))
0100
=== added symlink 'actions/charmhelpers'
=== target is u'../charmhelpers'
=== added symlink 'actions/pause'
=== target is u'actions.py'
=== added symlink 'actions/percona_utils.py'
=== target is u'../hooks/percona_utils.py'
=== added symlink 'actions/resume'
=== target is u'actions.py'
=== added file 'charm-helpers-hooks.yaml'
--- charm-helpers-hooks.yaml 1970-01-01 00:00:00 +0000
+++ charm-helpers-hooks.yaml 2016-03-10 22:55:31 +0000
@@ -0,0 +1,12 @@
1branch: lp:charm-helpers
2destination: hooks/charmhelpers
3include:
4 - core
5 - cli
6 - fetch
7 - contrib.hahelpers.cluster
8 - contrib.peerstorage
9 - payload.execd
10 - contrib.network.ip
11 - contrib.database
12 - contrib.charmsupport
013
=== renamed file 'charm-helpers-hooks.yaml' => 'charm-helpers-hooks.yaml.moved'
=== added file 'charm-helpers-tests.yaml'
--- charm-helpers-tests.yaml 1970-01-01 00:00:00 +0000
+++ charm-helpers-tests.yaml 2016-03-10 22:55:31 +0000
@@ -0,0 +1,6 @@
1branch: lp:charm-helpers
2destination: tests/charmhelpers
3include:
4 - contrib.amulet
5 - contrib.openstack.amulet
6 - core.hookenv
07
=== renamed file 'charm-helpers-tests.yaml' => 'charm-helpers-tests.yaml.moved'
=== added directory 'charmhelpers'
=== renamed directory 'charmhelpers' => 'charmhelpers.moved'
=== added file 'charmhelpers/__init__.py'
--- charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,38 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.
19import subprocess
20import sys
21
22try:
23 import six # flake8: noqa
24except ImportError:
25 if sys.version_info.major == 2:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
27 else:
28 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
29 import six # flake8: noqa
30
31try:
32 import yaml # flake8: noqa
33except ImportError:
34 if sys.version_info.major == 2:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
36 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa
039
=== added directory 'charmhelpers/cli'
=== added file 'charmhelpers/cli/__init__.py'
--- charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/cli/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,191 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import inspect
18import argparse
19import sys
20
21from six.moves import zip
22
23import charmhelpers.core.unitdata
24
25
26class OutputFormatter(object):
27 def __init__(self, outfile=sys.stdout):
28 self.formats = (
29 "raw",
30 "json",
31 "py",
32 "yaml",
33 "csv",
34 "tab",
35 )
36 self.outfile = outfile
37
38 def add_arguments(self, argument_parser):
39 formatgroup = argument_parser.add_mutually_exclusive_group()
40 choices = self.supported_formats
41 formatgroup.add_argument("--format", metavar='FMT',
42 help="Select output format for returned data, "
43 "where FMT is one of: {}".format(choices),
44 choices=choices, default='raw')
45 for fmt in self.formats:
46 fmtfunc = getattr(self, fmt)
47 formatgroup.add_argument("-{}".format(fmt[0]),
48 "--{}".format(fmt), action='store_const',
49 const=fmt, dest='format',
50 help=fmtfunc.__doc__)
51
52 @property
53 def supported_formats(self):
54 return self.formats
55
56 def raw(self, output):
57 """Output data as raw string (default)"""
58 if isinstance(output, (list, tuple)):
59 output = '\n'.join(map(str, output))
60 self.outfile.write(str(output))
61
62 def py(self, output):
63 """Output data as a nicely-formatted python data structure"""
64 import pprint
65 pprint.pprint(output, stream=self.outfile)
66
67 def json(self, output):
68 """Output data in JSON format"""
69 import json
70 json.dump(output, self.outfile)
71
72 def yaml(self, output):
73 """Output data in YAML format"""
74 import yaml
75 yaml.safe_dump(output, self.outfile)
76
77 def csv(self, output):
78 """Output data as excel-compatible CSV"""
79 import csv
80 csvwriter = csv.writer(self.outfile)
81 csvwriter.writerows(output)
82
83 def tab(self, output):
84 """Output data in excel-compatible tab-delimited format"""
85 import csv
86 csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
87 csvwriter.writerows(output)
88
89 def format_output(self, output, fmt='raw'):
90 fmtfunc = getattr(self, fmt)
91 fmtfunc(output)
92
93
94class CommandLine(object):
95 argument_parser = None
96 subparsers = None
97 formatter = None
98 exit_code = 0
99
100 def __init__(self):
101 if not self.argument_parser:
102 self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
103 if not self.formatter:
104 self.formatter = OutputFormatter()
105 self.formatter.add_arguments(self.argument_parser)
106 if not self.subparsers:
107 self.subparsers = self.argument_parser.add_subparsers(help='Commands')
108
109 def subcommand(self, command_name=None):
110 """
111 Decorate a function as a subcommand. Use its arguments as the
112 command-line arguments"""
113 def wrapper(decorated):
114 cmd_name = command_name or decorated.__name__
115 subparser = self.subparsers.add_parser(cmd_name,
116 description=decorated.__doc__)
117 for args, kwargs in describe_arguments(decorated):
118 subparser.add_argument(*args, **kwargs)
119 subparser.set_defaults(func=decorated)
120 return decorated
121 return wrapper
122
123 def test_command(self, decorated):
124 """
125 Subcommand is a boolean test function, so bool return values should be
126 converted to a 0/1 exit code.
127 """
128 decorated._cli_test_command = True
129 return decorated
130
131 def no_output(self, decorated):
132 """
133 Subcommand is not expected to return a value, so don't print a spurious None.
134 """
135 decorated._cli_no_output = True
136 return decorated
137
138 def subcommand_builder(self, command_name, description=None):
139 """
140 Decorate a function that builds a subcommand. Builders should accept a
141 single argument (the subparser instance) and return the function to be
142 run as the command."""
143 def wrapper(decorated):
144 subparser = self.subparsers.add_parser(command_name)
145 func = decorated(subparser)
146 subparser.set_defaults(func=func)
147 subparser.description = description or func.__doc__
148 return wrapper
149
150 def run(self):
151 "Run cli, processing arguments and executing subcommands."
152 arguments = self.argument_parser.parse_args()
153 argspec = inspect.getargspec(arguments.func)
154 vargs = []
155 for arg in argspec.args:
156 vargs.append(getattr(arguments, arg))
157 if argspec.varargs:
158 vargs.extend(getattr(arguments, argspec.varargs))
159 output = arguments.func(*vargs)
160 if getattr(arguments.func, '_cli_test_command', False):
161 self.exit_code = 0 if output else 1
162 output = ''
163 if getattr(arguments.func, '_cli_no_output', False):
164 output = ''
165 self.formatter.format_output(output, arguments.format)
166 if charmhelpers.core.unitdata._KV:
167 charmhelpers.core.unitdata._KV.flush()
168
169
170cmdline = CommandLine()
171
172
173def describe_arguments(func):
174 """
175 Analyze a function's signature and return a data structure suitable for
176 passing in as arguments to an argparse parser's add_argument() method."""
177
178 argspec = inspect.getargspec(func)
179 # we should probably raise an exception somewhere if func includes **kwargs
180 if argspec.defaults:
181 positional_args = argspec.args[:-len(argspec.defaults)]
182 keyword_names = argspec.args[-len(argspec.defaults):]
183 for arg, default in zip(keyword_names, argspec.defaults):
184 yield ('--{}'.format(arg),), {'default': default}
185 else:
186 positional_args = argspec.args
187
188 for arg in positional_args:
189 yield (arg,), {}
190 if argspec.varargs:
191 yield (argspec.varargs,), {'nargs': '*'}
0192
=== added file 'charmhelpers/cli/benchmark.py'
--- charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/cli/benchmark.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,36 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.contrib.benchmark import Benchmark
19
20
21@cmdline.subcommand(command_name='benchmark-start')
22def start():
23 Benchmark.start()
24
25
26@cmdline.subcommand(command_name='benchmark-finish')
27def finish():
28 Benchmark.finish()
29
30
31@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
32def service(subparser):
33 subparser.add_argument("value", help="The composite score.")
34 subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
35 subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
36 return Benchmark.set_composite_score
037
=== added file 'charmhelpers/cli/commands.py'
--- charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/cli/commands.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,32 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""
18This module loads sub-modules into the python runtime so they can be
19discovered via the inspect module. In order to prevent flake8 from (rightfully)
20telling us these are unused modules, throw a ' # noqa' at the end of each import
21so that the warning is suppressed.
22"""
23
24from . import CommandLine # noqa
25
26"""
27Import the sub-modules which have decorated subcommands to register with chlp.
28"""
29from . import host # noqa
30from . import benchmark # noqa
31from . import unitdata # noqa
32from . import hookenv # noqa
033
=== added file 'charmhelpers/cli/hookenv.py'
--- charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/cli/hookenv.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,23 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.core import hookenv
19
20
21cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
22cmdline.subcommand('service-name')(hookenv.service_name)
23cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
024
=== added file 'charmhelpers/cli/host.py'
--- charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/cli/host.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,31 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.core import host
19
20
21@cmdline.subcommand()
22def mounts():
23 "List mounts"
24 return host.mounts()
25
26
27@cmdline.subcommand_builder('service', description="Control system services")
28def service(subparser):
29 subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
30 subparser.add_argument("service_name", help="Name of the service to control")
31 return host.service
032
=== added file 'charmhelpers/cli/unitdata.py'
--- charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/cli/unitdata.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,39 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from . import cmdline
18from charmhelpers.core import unitdata
19
20
21@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
22def unitdata_cmd(subparser):
23 nested = subparser.add_subparsers()
24 get_cmd = nested.add_parser('get', help='Retrieve data')
25 get_cmd.add_argument('key', help='Key to retrieve the value of')
26 get_cmd.set_defaults(action='get', value=None)
27 set_cmd = nested.add_parser('set', help='Store data')
28 set_cmd.add_argument('key', help='Key to set')
29 set_cmd.add_argument('value', help='Value to store')
30 set_cmd.set_defaults(action='set')
31
32 def _unitdata_cmd(action, key, value):
33 if action == 'get':
34 return unitdata.kv().get(key)
35 elif action == 'set':
36 unitdata.kv().set(key, value)
37 unitdata.kv().flush()
38 return ''
39 return _unitdata_cmd
040
=== added directory 'charmhelpers/contrib'
=== added file 'charmhelpers/contrib/__init__.py'
--- charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added directory 'charmhelpers/contrib/charmsupport'
=== added file 'charmhelpers/contrib/charmsupport/__init__.py'
--- charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/charmsupport/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'charmhelpers/contrib/charmsupport/nrpe.py'
--- charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/charmsupport/nrpe.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,398 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""Compatibility with the nrpe-external-master charm"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Matthew Wedgwood <matthew.wedgwood@canonical.com>
22
23import subprocess
24import pwd
25import grp
26import os
27import glob
28import shutil
29import re
30import shlex
31import yaml
32
33from charmhelpers.core.hookenv import (
34 config,
35 local_unit,
36 log,
37 relation_ids,
38 relation_set,
39 relations_of_type,
40)
41
42from charmhelpers.core.host import service
43
44# This module adds compatibility with the nrpe-external-master and plain nrpe
45# subordinate charms. To use it in your charm:
46#
47# 1. Update metadata.yaml
48#
49# provides:
50# (...)
51# nrpe-external-master:
52# interface: nrpe-external-master
53# scope: container
54#
55# and/or
56#
57# provides:
58# (...)
59# local-monitors:
60# interface: local-monitors
61# scope: container
62
63#
64# 2. Add the following to config.yaml
65#
66# nagios_context:
67# default: "juju"
68# type: string
69# description: |
70# Used by the nrpe subordinate charms.
71# A string that will be prepended to instance name to set the host name
72# in nagios. So for instance the hostname would be something like:
73# juju-myservice-0
74# If you're running multiple environments with the same services in them
75# this allows you to differentiate between them.
76# nagios_servicegroups:
77# default: ""
78# type: string
79# description: |
80# A comma-separated list of nagios servicegroups.
81# If left empty, the nagios_context will be used as the servicegroup
82#
83# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
84#
85# 4. Update your hooks.py with something like this:
86#
87# from charmsupport.nrpe import NRPE
88# (...)
89# def update_nrpe_config():
90# nrpe_compat = NRPE()
91# nrpe_compat.add_check(
92# shortname = "myservice",
93# description = "Check MyService",
94# check_cmd = "check_http -w 2 -c 10 http://localhost"
95# )
96# nrpe_compat.add_check(
97# "myservice_other",
98# "Check for widget failures",
99# check_cmd = "/srv/myapp/scripts/widget_check"
100# )
101# nrpe_compat.write()
102#
103# def config_changed():
104# (...)
105# update_nrpe_config()
106#
107# def nrpe_external_master_relation_changed():
108# update_nrpe_config()
109#
110# def local_monitors_relation_changed():
111# update_nrpe_config()
112#
113# 5. ln -s hooks.py nrpe-external-master-relation-changed
114# ln -s hooks.py local-monitors-relation-changed
115
116
117class CheckException(Exception):
118 pass
119
120
121class Check(object):
122 shortname_re = '[A-Za-z0-9-_]+$'
123 service_template = ("""
124#---------------------------------------------------
125# This file is Juju managed
126#---------------------------------------------------
127define service {{
128 use active-service
129 host_name {nagios_hostname}
130 service_description {nagios_hostname}[{shortname}] """
131 """{description}
132 check_command check_nrpe!{command}
133 servicegroups {nagios_servicegroup}
134}}
135""")
136
137 def __init__(self, shortname, description, check_cmd):
138 super(Check, self).__init__()
139 # XXX: could be better to calculate this from the service name
140 if not re.match(self.shortname_re, shortname):
141 raise CheckException("shortname must match {}".format(
142 Check.shortname_re))
143 self.shortname = shortname
144 self.command = "check_{}".format(shortname)
145 # Note: a set of invalid characters is defined by the
146 # Nagios server config
147 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)
150
151 def _get_check_filename(self):
152 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
153
154 def _get_service_filename(self, hostname):
155 return os.path.join(NRPE.nagios_exportdir,
156 'service__{}_{}.cfg'.format(hostname, self.command))
157
158 def _locate_cmd(self, check_cmd):
159 search_path = (
160 '/usr/lib/nagios/plugins',
161 '/usr/local/lib/nagios/plugins',
162 )
163 parts = shlex.split(check_cmd)
164 for path in search_path:
165 if os.path.exists(os.path.join(path, parts[0])):
166 command = os.path.join(path, parts[0])
167 if len(parts) > 1:
168 command += " " + " ".join(parts[1:])
169 return command
170 log('Check command not found: {}'.format(parts[0]))
171 return ''
172
173 def _remove_service_files(self):
174 if not os.path.exists(NRPE.nagios_exportdir):
175 return
176 for f in os.listdir(NRPE.nagios_exportdir):
177 if f.endswith('_{}.cfg'.format(self.command)):
178 os.remove(os.path.join(NRPE.nagios_exportdir, f))
179
180 def remove(self, hostname):
181 nrpe_check_file = self._get_check_filename()
182 if os.path.exists(nrpe_check_file):
183 os.remove(nrpe_check_file)
184 self._remove_service_files()
185
186 def write(self, nagios_context, hostname, nagios_servicegroups):
187 nrpe_check_file = self._get_check_filename()
188 with open(nrpe_check_file, 'w') as nrpe_check_config:
189 nrpe_check_config.write("# check {}\n".format(self.shortname))
190 nrpe_check_config.write("command[{}]={}\n".format(
191 self.command, self.check_cmd))
192
193 if not os.path.exists(NRPE.nagios_exportdir):
194 log('Not writing service config as {} is not accessible'.format(
195 NRPE.nagios_exportdir))
196 else:
197 self.write_service_config(nagios_context, hostname,
198 nagios_servicegroups)
199
200 def write_service_config(self, nagios_context, hostname,
201 nagios_servicegroups):
202 self._remove_service_files()
203
204 templ_vars = {
205 'nagios_hostname': hostname,
206 'nagios_servicegroup': nagios_servicegroups,
207 'description': self.description,
208 'shortname': self.shortname,
209 'command': self.command,
210 }
211 nrpe_service_text = Check.service_template.format(**templ_vars)
212 nrpe_service_file = self._get_service_filename(hostname)
213 with open(nrpe_service_file, 'w') as nrpe_service_config:
214 nrpe_service_config.write(str(nrpe_service_text))
215
216 def run(self):
217 subprocess.call(self.check_cmd)
218
219
220class NRPE(object):
221 nagios_logdir = '/var/log/nagios'
222 nagios_exportdir = '/var/lib/nagios/export'
223 nrpe_confdir = '/etc/nagios/nrpe.d'
224
225 def __init__(self, hostname=None):
226 super(NRPE, self).__init__()
227 self.config = config()
228 self.nagios_context = self.config['nagios_context']
229 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
230 self.nagios_servicegroups = self.config['nagios_servicegroups']
231 else:
232 self.nagios_servicegroups = self.nagios_context
233 self.unit_name = local_unit().replace('/', '-')
234 if hostname:
235 self.hostname = hostname
236 else:
237 nagios_hostname = get_nagios_hostname()
238 if nagios_hostname:
239 self.hostname = nagios_hostname
240 else:
241 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
242 self.checks = []
243
244 def add_check(self, *args, **kwargs):
245 self.checks.append(Check(*args, **kwargs))
246
247 def remove_check(self, *args, **kwargs):
248 if kwargs.get('shortname') is None:
249 raise ValueError('shortname of check must be specified')
250
251 # Use sensible defaults if they're not specified - these are not
252 # actually used during removal, but they're required for constructing
253 # the Check object; check_disk is chosen because it's part of the
254 # nagios-plugins-basic package.
255 if kwargs.get('check_cmd') is None:
256 kwargs['check_cmd'] = 'check_disk'
257 if kwargs.get('description') is None:
258 kwargs['description'] = ''
259
260 check = Check(*args, **kwargs)
261 check.remove(self.hostname)
262
263 def write(self):
264 try:
265 nagios_uid = pwd.getpwnam('nagios').pw_uid
266 nagios_gid = grp.getgrnam('nagios').gr_gid
267 except:
268 log("Nagios user not set up, nrpe checks not updated")
269 return
270
271 if not os.path.exists(NRPE.nagios_logdir):
272 os.mkdir(NRPE.nagios_logdir)
273 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
274
275 nrpe_monitors = {}
276 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
277 for nrpecheck in self.checks:
278 nrpecheck.write(self.nagios_context, self.hostname,
279 self.nagios_servicegroups)
280 nrpe_monitors[nrpecheck.shortname] = {
281 "command": nrpecheck.command,
282 }
283
284 service('restart', 'nagios-nrpe-server')
285
286 monitor_ids = relation_ids("local-monitors") + \
287 relation_ids("nrpe-external-master")
288 for rid in monitor_ids:
289 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
290
291
292def get_nagios_hostcontext(relation_name='nrpe-external-master'):
293 """
294 Query relation with nrpe subordinate, return the nagios_host_context
295
296 :param str relation_name: Name of relation nrpe sub joined to
297 """
298 for rel in relations_of_type(relation_name):
299 if 'nagios_host_context' in rel:
300 return rel['nagios_host_context']
301
302
303def get_nagios_hostname(relation_name='nrpe-external-master'):
304 """
305 Query relation with nrpe subordinate, return the nagios_hostname
306
307 :param str relation_name: Name of relation nrpe sub joined to
308 """
309 for rel in relations_of_type(relation_name):
310 if 'nagios_hostname' in rel:
311 return rel['nagios_hostname']
312
313
314def get_nagios_unit_name(relation_name='nrpe-external-master'):
315 """
316 Return the nagios unit name prepended with host_context if needed
317
318 :param str relation_name: Name of relation nrpe sub joined to
319 """
320 host_context = get_nagios_hostcontext(relation_name)
321 if host_context:
322 unit = "%s:%s" % (host_context, local_unit())
323 else:
324 unit = local_unit()
325 return unit
326
327
328def add_init_service_checks(nrpe, services, unit_name):
329 """
330 Add checks for each service in list
331
332 :param NRPE nrpe: NRPE object to add check to
333 :param list services: List of services to check
334 :param str unit_name: Unit name to use in check description
335 """
336 for svc in services:
337 upstart_init = '/etc/init/%s.conf' % svc
338 sysv_init = '/etc/init.d/%s' % svc
339 if os.path.exists(upstart_init):
340 # Don't add a check for these services from neutron-gateway
341 if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
342 nrpe.add_check(
343 shortname=svc,
344 description='process check {%s}' % unit_name,
345 check_cmd='check_upstart_job %s' % svc
346 )
347 elif os.path.exists(sysv_init):
348 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
349 cron_file = ('*/5 * * * * root '
350 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
351 '-s /etc/init.d/%s status > '
352 '/var/lib/nagios/service-check-%s.txt\n' % (svc,
353 svc)
354 )
355 f = open(cronpath, 'w')
356 f.write(cron_file)
357 f.close()
358 nrpe.add_check(
359 shortname=svc,
360 description='process check {%s}' % unit_name,
361 check_cmd='check_status_file.py -f '
362 '/var/lib/nagios/service-check-%s.txt' % svc,
363 )
364
365
366def copy_nrpe_checks():
367 """
368 Copy the nrpe checks into place
369
370 """
371 NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
372 nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
373 'charmhelpers', 'contrib', 'openstack',
374 'files')
375
376 if not os.path.exists(NAGIOS_PLUGINS):
377 os.makedirs(NAGIOS_PLUGINS)
378 for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
379 if os.path.isfile(fname):
380 shutil.copy2(fname,
381 os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
382
383
384def add_haproxy_checks(nrpe, unit_name):
385 """
386 Add checks for each service in list
387
388 :param NRPE nrpe: NRPE object to add check to
389 :param str unit_name: Unit name to use in check description
390 """
391 nrpe.add_check(
392 shortname='haproxy_servers',
393 description='Check HAProxy {%s}' % unit_name,
394 check_cmd='check_haproxy.sh')
395 nrpe.add_check(
396 shortname='haproxy_queue',
397 description='Check HAProxy queue depth {%s}' % unit_name,
398 check_cmd='check_haproxy_queue_depth.sh')
0399
=== added file 'charmhelpers/contrib/charmsupport/volumes.py'
--- charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/charmsupport/volumes.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,175 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17'''
18Functions for managing volumes in juju units. One volume is supported per unit.
19Subordinates may have their own storage, provided it is on its own partition.
20
21Configuration stanzas::
22
23 volume-ephemeral:
24 type: boolean
25 default: true
26 description: >
27 If false, a volume is mounted as sepecified in "volume-map"
28 If true, ephemeral storage will be used, meaning that log data
29 will only exist as long as the machine. YOU HAVE BEEN WARNED.
30 volume-map:
31 type: string
32 default: {}
33 description: >
34 YAML map of units to device names, e.g:
35 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36 Service units will raise a configure-error if volume-ephemeral
37 is 'true' and no volume-map value is set. Use 'juju set' to set a
38 value and 'juju resolved' to complete configuration.
39
40Usage::
41
42 from charmsupport.volumes import configure_volume, VolumeConfigurationError
43 from charmsupport.hookenv import log, ERROR
44 def post_mount_hook():
45 stop_service('myservice')
46 def post_mount_hook():
47 start_service('myservice')
48
49 if __name__ == '__main__':
50 try:
51 configure_volume(before_change=pre_mount_hook,
52 after_change=post_mount_hook)
53 except VolumeConfigurationError:
54 log('Storage could not be configured', ERROR)
55
56'''
57
58# XXX: Known limitations
59# - fstab is neither consulted nor updated
60
61import os
62from charmhelpers.core import hookenv
63from charmhelpers.core import host
64import yaml
65
66
67MOUNT_BASE = '/srv/juju/volumes'
68
69
70class VolumeConfigurationError(Exception):
71 '''Volume configuration data is missing or invalid'''
72 pass
73
74
75def get_config():
76 '''Gather and sanity-check volume configuration data'''
77 volume_config = {}
78 config = hookenv.config()
79
80 errors = False
81
82 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83 volume_config['ephemeral'] = True
84 else:
85 volume_config['ephemeral'] = False
86
87 try:
88 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89 except yaml.YAMLError as e:
90 hookenv.log("Error parsing YAML volume-map: {}".format(e),
91 hookenv.ERROR)
92 errors = True
93 if volume_map is None:
94 # probably an empty string
95 volume_map = {}
96 elif not isinstance(volume_map, dict):
97 hookenv.log("Volume-map should be a dictionary, not {}".format(
98 type(volume_map)))
99 errors = True
100
101 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102 if volume_config['device'] and volume_config['ephemeral']:
103 # asked for ephemeral storage but also defined a volume ID
104 hookenv.log('A volume is defined for this unit, but ephemeral '
105 'storage was requested', hookenv.ERROR)
106 errors = True
107 elif not volume_config['device'] and not volume_config['ephemeral']:
108 # asked for permanent storage but did not define volume ID
109 hookenv.log('Ephemeral storage was requested, but there is no volume '
110 'defined for this unit.', hookenv.ERROR)
111 errors = True
112
113 unit_mount_name = hookenv.local_unit().replace('/', '-')
114 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
115
116 if errors:
117 return None
118 return volume_config
119
120
121def mount_volume(config):
122 if os.path.exists(config['mountpoint']):
123 if not os.path.isdir(config['mountpoint']):
124 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125 raise VolumeConfigurationError()
126 else:
127 host.mkdir(config['mountpoint'])
128 if os.path.ismount(config['mountpoint']):
129 unmount_volume(config)
130 if not host.mount(config['device'], config['mountpoint'], persist=True):
131 raise VolumeConfigurationError()
132
133
134def unmount_volume(config):
135 if os.path.ismount(config['mountpoint']):
136 if not host.umount(config['mountpoint'], persist=True):
137 raise VolumeConfigurationError()
138
139
140def managed_mounts():
141 '''List of all mounted managed volumes'''
142 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
143
144
145def configure_volume(before_change=lambda: None, after_change=lambda: None):
146 '''Set up storage (or don't) according to the charm's volume configuration.
147 Returns the mount point or "ephemeral". before_change and after_change
148 are optional functions to be called if the volume configuration changes.
149 '''
150
151 config = get_config()
152 if not config:
153 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154 raise VolumeConfigurationError()
155
156 if config['ephemeral']:
157 if os.path.ismount(config['mountpoint']):
158 before_change()
159 unmount_volume(config)
160 after_change()
161 return 'ephemeral'
162 else:
163 # persistent storage
164 if os.path.ismount(config['mountpoint']):
165 mounts = dict(managed_mounts())
166 if mounts.get(config['mountpoint']) != config['device']:
167 before_change()
168 unmount_volume(config)
169 mount_volume(config)
170 after_change()
171 else:
172 before_change()
173 mount_volume(config)
174 after_change()
175 return config['mountpoint']
0176
=== added directory 'charmhelpers/contrib/database'
=== added file 'charmhelpers/contrib/database/__init__.py'
=== added file 'charmhelpers/contrib/database/mysql.py'
--- charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/database/mysql.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,415 @@
1"""Helper for working with a MySQL database"""
2import json
3import re
4import sys
5import platform
6import os
7import glob
8
9# from string import upper
10
11from charmhelpers.core.host import (
12 mkdir,
13 pwgen,
14 write_file
15)
16from charmhelpers.core.hookenv import (
17 config as config_get,
18 relation_get,
19 related_units,
20 unit_get,
21 log,
22 DEBUG,
23 INFO,
24 WARNING,
25)
26from charmhelpers.fetch import (
27 apt_install,
28 apt_update,
29 filter_installed_packages,
30)
31from charmhelpers.contrib.peerstorage import (
32 peer_store,
33 peer_retrieve,
34)
35from charmhelpers.contrib.network.ip import get_host_ip
36
37try:
38 import MySQLdb
39except ImportError:
40 apt_update(fatal=True)
41 apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
42 import MySQLdb
43
44
45class MySQLHelper(object):
46
47 def __init__(self, rpasswdf_template, upasswdf_template, host='localhost',
48 migrate_passwd_to_peer_relation=True,
49 delete_ondisk_passwd_file=True):
50 self.host = host
51 # Password file path templates
52 self.root_passwd_file_template = rpasswdf_template
53 self.user_passwd_file_template = upasswdf_template
54
55 self.migrate_passwd_to_peer_relation = migrate_passwd_to_peer_relation
56 # If we migrate we have the option to delete local copy of root passwd
57 self.delete_ondisk_passwd_file = delete_ondisk_passwd_file
58
59 def connect(self, user='root', password=None):
60 log("Opening db connection for %s@%s" % (user, self.host), level=DEBUG)
61 if password==None:
62 self.connection = MySQLdb.connect(user=user, host=self.host)
63 else:
64 self.connection = MySQLdb.connect(user=user, host=self.host,
65 passwd=password)
66
67 def database_exists(self, db_name):
68 cursor = self.connection.cursor()
69 try:
70 cursor.execute("SHOW DATABASES")
71 databases = [i[0] for i in cursor.fetchall()]
72 finally:
73 cursor.close()
74
75 return db_name in databases
76
77 def create_database(self, db_name):
78 cursor = self.connection.cursor()
79 try:
80 cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
81 .format(db_name))
82 finally:
83 cursor.close()
84
85 def grant_exists(self, db_name, db_user, remote_ip):
86 cursor = self.connection.cursor()
87 priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
88 "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
89 try:
90 cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
91 remote_ip))
92 grants = [i[0] for i in cursor.fetchall()]
93 except MySQLdb.OperationalError:
94 return False
95 finally:
96 cursor.close()
97
98 # TODO: review for different grants
99 return priv_string in grants
100
101 def create_grant(self, db_name, db_user, remote_ip, password):
102 cursor = self.connection.cursor()
103 try:
104 # TODO: review for different grants
105 cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
106 "IDENTIFIED BY '{}'".format(db_name,
107 db_user,
108 remote_ip,
109 password))
110 finally:
111 cursor.close()
112
113 def create_admin_grant(self, db_user, remote_ip, password):
114 cursor = self.connection.cursor()
115 try:
116 cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
117 "IDENTIFIED BY '{}'".format(db_user,
118 remote_ip,
119 password))
120 finally:
121 cursor.close()
122
123 def cleanup_grant(self, db_user, remote_ip):
124 cursor = self.connection.cursor()
125 try:
126 cursor.execute("DROP FROM mysql.user WHERE user='{}' "
127 "AND HOST='{}'".format(db_user,
128 remote_ip))
129 finally:
130 cursor.close()
131
132 def execute(self, sql):
133 """Execute arbitary SQL against the database."""
134 cursor = self.connection.cursor()
135 try:
136 cursor.execute(sql)
137 finally:
138 cursor.close()
139
140 def migrate_passwords_to_peer_relation(self, excludes=None):
141 """Migrate any passwords storage on disk to cluster peer relation."""
142 dirname = os.path.dirname(self.root_passwd_file_template)
143 path = os.path.join(dirname, '*.passwd')
144 for f in glob.glob(path):
145 if excludes and f in excludes:
146 log("Excluding %s from peer migration" % (f), level=DEBUG)
147 continue
148
149 key = os.path.basename(f)
150 with open(f, 'r') as passwd:
151 _value = passwd.read().strip()
152
153 try:
154 peer_store(key, _value)
155
156 if self.delete_ondisk_passwd_file:
157 os.unlink(f)
158 except ValueError:
159 # NOTE cluster relation not yet ready - skip for now
160 pass
161
162 def get_mysql_password_on_disk(self, username=None, password=None):
163 """Retrieve, generate or store a mysql password for the provided
164 username on disk."""
165 if username:
166 template = self.user_passwd_file_template
167 passwd_file = template.format(username)
168 else:
169 passwd_file = self.root_passwd_file_template
170
171 _password = None
172 if os.path.exists(passwd_file):
173 log("Using existing password file '%s'" % passwd_file, level=DEBUG)
174 with open(passwd_file, 'r') as passwd:
175 _password = passwd.read().strip()
176 else:
177 log("Generating new password file '%s'" % passwd_file, level=DEBUG)
178 if not os.path.isdir(os.path.dirname(passwd_file)):
179 # NOTE: need to ensure this is not mysql root dir (which needs
180 # to be mysql readable)
181 mkdir(os.path.dirname(passwd_file), owner='root', group='root',
182 perms=0o770)
183 # Force permissions - for some reason the chmod in makedirs
184 # fails
185 os.chmod(os.path.dirname(passwd_file), 0o770)
186
187 _password = password or pwgen(length=32)
188 write_file(passwd_file, _password, owner='root', group='root',
189 perms=0o660)
190
191 return _password
192
193 def passwd_keys(self, username):
194 """Generator to return keys used to store passwords in peer store.
195
196 NOTE: we support both legacy and new format to support mysql
197 charm prior to refactor. This is necessary to avoid LP 1451890.
198 """
199 keys = []
200 if username == 'mysql':
201 log("Bad username '%s'" % (username), level=WARNING)
202
203 if username:
204 # IMPORTANT: *newer* format must be returned first
205 keys.append('mysql-%s.passwd' % (username))
206 keys.append('%s.passwd' % (username))
207 else:
208 keys.append('mysql.passwd')
209
210 for key in keys:
211 yield key
212
213 def get_mysql_password(self, username=None, password=None):
214 """Retrieve, generate or store a mysql password for the provided
215 username using peer relation cluster."""
216 excludes = []
217
218 # First check peer relation.
219 try:
220 for key in self.passwd_keys(username):
221 _password = peer_retrieve(key)
222 if _password:
223 break
224
225 # If root password available don't update peer relation from local
226 if _password and not username:
227 excludes.append(self.root_passwd_file_template)
228
229 except ValueError:
230 # cluster relation is not yet started; use on-disk
231 _password = None
232
233 # If none available, generate new one
234 if not _password:
235 _password = self.get_mysql_password_on_disk(username, password)
236
237 # Put on wire if required
238 if self.migrate_passwd_to_peer_relation:
239 self.migrate_passwords_to_peer_relation(excludes=excludes)
240
241 return _password
242
243 def get_mysql_root_password(self, password=None):
244 """Retrieve or generate mysql root password for service units."""
245 return self.get_mysql_password(username=None, password=password)
246
247 def normalize_address(self, hostname):
248 """Ensure that address returned is an IP address (i.e. not fqdn)"""
249 if config_get('prefer-ipv6'):
250 # TODO: add support for ipv6 dns
251 return hostname
252
253 if hostname != unit_get('private-address'):
254 return get_host_ip(hostname, fallback=hostname)
255
256 # Otherwise assume localhost
257 return '127.0.0.1'
258
259 def get_allowed_units(self, database, username, relation_id=None):
260 """Get list of units with access grants for database with username.
261
262 This is typically used to provide shared-db relations with a list of
263 which units have been granted access to the given database.
264 """
265 self.connect(password=self.get_mysql_root_password())
266 allowed_units = set()
267 for unit in related_units(relation_id):
268 settings = relation_get(rid=relation_id, unit=unit)
269 # First check for setting with prefix, then without
270 for attr in ["%s_hostname" % (database), 'hostname']:
271 hosts = settings.get(attr, None)
272 if hosts:
273 break
274
275 if hosts:
276 # hostname can be json-encoded list of hostnames
277 try:
278 hosts = json.loads(hosts)
279 except ValueError:
280 hosts = [hosts]
281 else:
282 hosts = [settings['private-address']]
283
284 if hosts:
285 for host in hosts:
286 host = self.normalize_address(host)
287 if self.grant_exists(database, username, host):
288 log("Grant exists for host '%s' on db '%s'" %
289 (host, database), level=DEBUG)
290 if unit not in allowed_units:
291 allowed_units.add(unit)
292 else:
293 log("Grant does NOT exist for host '%s' on db '%s'" %
294 (host, database), level=DEBUG)
295 else:
296 log("No hosts found for grant check", level=INFO)
297
298 return allowed_units
299
300 def configure_db(self, hostname, database, username, admin=False):
301 """Configure access to database for username from hostname."""
302 self.connect(password=self.get_mysql_root_password())
303 if not self.database_exists(database):
304 self.create_database(database)
305
306 remote_ip = self.normalize_address(hostname)
307 password = self.get_mysql_password(username)
308 if not self.grant_exists(database, username, remote_ip):
309 if not admin:
310 self.create_grant(database, username, remote_ip, password)
311 else:
312 self.create_admin_grant(username, remote_ip, password)
313
314 return password
315
316
317class PerconaClusterHelper(object):
318
319 # Going for the biggest page size to avoid wasted bytes.
320 # InnoDB page size is 16MB
321
322 DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
323 DEFAULT_INNODB_BUFFER_FACTOR = 0.50
324
325 def human_to_bytes(self, human):
326 """Convert human readable configuration options to bytes."""
327 num_re = re.compile('^[0-9]+$')
328 if num_re.match(human):
329 return human
330
331 factors = {
332 'K': 1024,
333 'M': 1048576,
334 'G': 1073741824,
335 'T': 1099511627776
336 }
337 modifier = human[-1]
338 if modifier in factors:
339 return int(human[:-1]) * factors[modifier]
340
341 if modifier == '%':
342 total_ram = self.human_to_bytes(self.get_mem_total())
343 if self.is_32bit_system() and total_ram > self.sys_mem_limit():
344 total_ram = self.sys_mem_limit()
345 factor = int(human[:-1]) * 0.01
346 pctram = total_ram * factor
347 return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
348
349 raise ValueError("Can only convert K,M,G, or T")
350
351 def is_32bit_system(self):
352 """Determine whether system is 32 or 64 bit."""
353 try:
354 return sys.maxsize < 2 ** 32
355 except OverflowError:
356 return False
357
358 def sys_mem_limit(self):
359 """Determine the default memory limit for the current service unit."""
360 if platform.machine() in ['armv7l']:
361 _mem_limit = self.human_to_bytes('2700M') # experimentally determined
362 else:
363 # Limit for x86 based 32bit systems
364 _mem_limit = self.human_to_bytes('4G')
365
366 return _mem_limit
367
368 def get_mem_total(self):
369 """Calculate the total memory in the current service unit."""
370 with open('/proc/meminfo') as meminfo_file:
371 for line in meminfo_file:
372 key, mem = line.split(':', 2)
373 if key == 'MemTotal':
374 mtot, modifier = mem.strip().split(' ')
375 return '%s%s' % (mtot, modifier[0].upper())
376
377 def parse_config(self):
378 """Parse charm configuration and calculate values for config files."""
379 config = config_get()
380 mysql_config = {}
381 if 'max-connections' in config:
382 mysql_config['max_connections'] = config['max-connections']
383
384 if 'wait-timeout' in config:
385 mysql_config['wait_timeout'] = config['wait-timeout']
386
387 if 'innodb-flush-log-at-trx-commit' in config:
388 mysql_config['innodb_flush_log_at_trx_commit'] = config['innodb-flush-log-at-trx-commit']
389
390 # Set a sane default key_buffer size
391 mysql_config['key_buffer'] = self.human_to_bytes('32M')
392 total_memory = self.human_to_bytes(self.get_mem_total())
393
394 dataset_bytes = config.get('dataset-size', None)
395 innodb_buffer_pool_size = config.get('innodb-buffer-pool-size', None)
396
397 if innodb_buffer_pool_size:
398 innodb_buffer_pool_size = self.human_to_bytes(
399 innodb_buffer_pool_size)
400 elif dataset_bytes:
401 log("Option 'dataset-size' has been deprecated, please use"
402 "innodb_buffer_pool_size option instead", level="WARN")
403 innodb_buffer_pool_size = self.human_to_bytes(
404 dataset_bytes)
405 else:
406 innodb_buffer_pool_size = int(
407 total_memory * self.DEFAULT_INNODB_BUFFER_FACTOR)
408
409 if innodb_buffer_pool_size > total_memory:
410 log("innodb_buffer_pool_size; {} is greater than system available memory:{}".format(
411 innodb_buffer_pool_size,
412 total_memory), level='WARN')
413
414 mysql_config['innodb_buffer_pool_size'] = innodb_buffer_pool_size
415 return mysql_config
0416
=== added directory 'charmhelpers/contrib/hahelpers'
=== added file 'charmhelpers/contrib/hahelpers/__init__.py'
--- charmhelpers/contrib/hahelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/hahelpers/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'charmhelpers/contrib/hahelpers/cluster.py'
--- charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/hahelpers/cluster.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,316 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# James Page <james.page@ubuntu.com>
22# Adam Gandelman <adamg@ubuntu.com>
23#
24
25"""
26Helpers for clustering and determining "cluster leadership" and other
27clustering-related helpers.
28"""
29
30import subprocess
31import os
32
33from socket import gethostname as get_unit_hostname
34
35import six
36
37from charmhelpers.core.hookenv import (
38 log,
39 relation_ids,
40 related_units as relation_list,
41 relation_get,
42 config as config_get,
43 INFO,
44 ERROR,
45 WARNING,
46 unit_get,
47 is_leader as juju_is_leader
48)
49from charmhelpers.core.decorators import (
50 retry_on_exception,
51)
52from charmhelpers.core.strutils import (
53 bool_from_string,
54)
55
56DC_RESOURCE_NAME = 'DC'
57
58
59class HAIncompleteConfig(Exception):
60 pass
61
62
63class CRMResourceNotFound(Exception):
64 pass
65
66
67class CRMDCNotFound(Exception):
68 pass
69
70
71def is_elected_leader(resource):
72 """
73 Returns True if the charm executing this is the elected cluster leader.
74
75 It relies on two mechanisms to determine leadership:
76 1. If juju is sufficiently new and leadership election is supported,
77 the is_leader command will be used.
78 2. If the charm is part of a corosync cluster, call corosync to
79 determine leadership.
80 3. If the charm is not part of a corosync cluster, the leader is
81 determined as being "the alive unit with the lowest unit numer". In
82 other words, the oldest surviving unit.
83 """
84 try:
85 return juju_is_leader()
86 except NotImplementedError:
87 log('Juju leadership election feature not enabled'
88 ', using fallback support',
89 level=WARNING)
90
91 if is_clustered():
92 if not is_crm_leader(resource):
93 log('Deferring action to CRM leader.', level=INFO)
94 return False
95 else:
96 peers = peer_units()
97 if peers and not oldest_peer(peers):
98 log('Deferring action to oldest service unit.', level=INFO)
99 return False
100 return True
101
102
103def is_clustered():
104 for r_id in (relation_ids('ha') or []):
105 for unit in (relation_list(r_id) or []):
106 clustered = relation_get('clustered',
107 rid=r_id,
108 unit=unit)
109 if clustered:
110 return True
111 return False
112
113
114def is_crm_dc():
115 """
116 Determine leadership by querying the pacemaker Designated Controller
117 """
118 cmd = ['crm', 'status']
119 try:
120 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
121 if not isinstance(status, six.text_type):
122 status = six.text_type(status, "utf-8")
123 except subprocess.CalledProcessError as ex:
124 raise CRMDCNotFound(str(ex))
125
126 current_dc = ''
127 for line in status.split('\n'):
128 if line.startswith('Current DC'):
129 # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
130 current_dc = line.split(':')[1].split()[0]
131 if current_dc == get_unit_hostname():
132 return True
133 elif current_dc == 'NONE':
134 raise CRMDCNotFound('Current DC: NONE')
135
136 return False
137
138
139@retry_on_exception(5, base_delay=2,
140 exc_type=(CRMResourceNotFound, CRMDCNotFound))
141def is_crm_leader(resource, retry=False):
142 """
143 Returns True if the charm calling this is the elected corosync leader,
144 as returned by calling the external "crm" command.
145
146 We allow this operation to be retried to avoid the possibility of getting a
147 false negative. See LP #1396246 for more info.
148 """
149 if resource == DC_RESOURCE_NAME:
150 return is_crm_dc()
151 cmd = ['crm', 'resource', 'show', resource]
152 try:
153 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
154 if not isinstance(status, six.text_type):
155 status = six.text_type(status, "utf-8")
156 except subprocess.CalledProcessError:
157 status = None
158
159 if status and get_unit_hostname() in status:
160 return True
161
162 if status and "resource %s is NOT running" % (resource) in status:
163 raise CRMResourceNotFound("CRM resource %s not found" % (resource))
164
165 return False
166
167
168def is_leader(resource):
169 log("is_leader is deprecated. Please consider using is_crm_leader "
170 "instead.", level=WARNING)
171 return is_crm_leader(resource)
172
173
174def peer_units(peer_relation="cluster"):
175 peers = []
176 for r_id in (relation_ids(peer_relation) or []):
177 for unit in (relation_list(r_id) or []):
178 peers.append(unit)
179 return peers
180
181
182def peer_ips(peer_relation='cluster', addr_key='private-address'):
183 '''Return a dict of peers and their private-address'''
184 peers = {}
185 for r_id in relation_ids(peer_relation):
186 for unit in relation_list(r_id):
187 peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
188 return peers
189
190
191def oldest_peer(peers):
192 """Determines who the oldest peer is by comparing unit numbers."""
193 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
194 for peer in peers:
195 remote_unit_no = int(peer.split('/')[1])
196 if remote_unit_no < local_unit_no:
197 return False
198 return True
199
200
201def eligible_leader(resource):
202 log("eligible_leader is deprecated. Please consider using "
203 "is_elected_leader instead.", level=WARNING)
204 return is_elected_leader(resource)
205
206
207def https():
208 '''
209 Determines whether enough data has been provided in configuration
210 or relation data to configure HTTPS
211 .
212 returns: boolean
213 '''
214 use_https = config_get('use-https')
215 if use_https and bool_from_string(use_https):
216 return True
217 if config_get('ssl_cert') and config_get('ssl_key'):
218 return True
219 for r_id in relation_ids('identity-service'):
220 for unit in relation_list(r_id):
221 # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
222 rel_state = [
223 relation_get('https_keystone', rid=r_id, unit=unit),
224 relation_get('ca_cert', rid=r_id, unit=unit),
225 ]
226 # NOTE: works around (LP: #1203241)
227 if (None not in rel_state) and ('' not in rel_state):
228 return True
229 return False
230
231
232def determine_api_port(public_port, singlenode_mode=False):
233 '''
234 Determine correct API server listening port based on
235 existence of HTTPS reverse proxy and/or haproxy.
236
237 public_port: int: standard public port for given service
238
239 singlenode_mode: boolean: Shuffle ports when only a single unit is present
240
241 returns: int: the correct listening port for the API service
242 '''
243 i = 0
244 if singlenode_mode:
245 i += 1
246 elif len(peer_units()) > 0 or is_clustered():
247 i += 1
248 if https():
249 i += 1
250 return public_port - (i * 10)
251
252
253def determine_apache_port(public_port, singlenode_mode=False):
254 '''
255 Description: Determine correct apache listening port based on public IP +
256 state of the cluster.
257
258 public_port: int: standard public port for given service
259
260 singlenode_mode: boolean: Shuffle ports when only a single unit is present
261
262 returns: int: the correct listening port for the HAProxy service
263 '''
264 i = 0
265 if singlenode_mode:
266 i += 1
267 elif len(peer_units()) > 0 or is_clustered():
268 i += 1
269 return public_port - (i * 10)
270
271
272def get_hacluster_config(exclude_keys=None):
273 '''
274 Obtains all relevant configuration from charm configuration required
275 for initiating a relation to hacluster:
276
277 ha-bindiface, ha-mcastport, vip
278
279 param: exclude_keys: list of setting key(s) to be excluded.
280 returns: dict: A dict containing settings keyed by setting name.
281 raises: HAIncompleteConfig if settings are missing.
282 '''
283 settings = ['ha-bindiface', 'ha-mcastport', 'vip']
284 conf = {}
285 for setting in settings:
286 if exclude_keys and setting in exclude_keys:
287 continue
288
289 conf[setting] = config_get(setting)
290 missing = []
291 [missing.append(s) for s, v in six.iteritems(conf) if v is None]
292 if missing:
293 log('Insufficient config data to configure hacluster.', level=ERROR)
294 raise HAIncompleteConfig
295 return conf
296
297
298def canonical_url(configs, vip_setting='vip'):
299 '''
300 Returns the correct HTTP URL to this host given the state of HTTPS
301 configuration and hacluster.
302
303 :configs : OSTemplateRenderer: A config tempating object to inspect for
304 a complete https context.
305
306 :vip_setting: str: Setting in charm config that specifies
307 VIP address.
308 '''
309 scheme = 'http'
310 if 'https' in configs.complete_contexts():
311 scheme = 'https'
312 if is_clustered():
313 addr = config_get(vip_setting)
314 else:
315 addr = unit_get('private-address')
316 return '%s://%s' % (scheme, addr)
0317
=== added directory 'charmhelpers/contrib/network'
=== added file 'charmhelpers/contrib/network/__init__.py'
--- charmhelpers/contrib/network/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/network/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'charmhelpers/contrib/network/ip.py'
--- charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/network/ip.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,458 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import glob
18import re
19import subprocess
20import six
21import socket
22
23from functools import partial
24
25from charmhelpers.core.hookenv import unit_get
26from charmhelpers.fetch import apt_install, apt_update
27from charmhelpers.core.hookenv import (
28 log,
29 WARNING,
30)
31
32try:
33 import netifaces
34except ImportError:
35 apt_update(fatal=True)
36 apt_install('python-netifaces', fatal=True)
37 import netifaces
38
39try:
40 import netaddr
41except ImportError:
42 apt_update(fatal=True)
43 apt_install('python-netaddr', fatal=True)
44 import netaddr
45
46
47def _validate_cidr(network):
48 try:
49 netaddr.IPNetwork(network)
50 except (netaddr.core.AddrFormatError, ValueError):
51 raise ValueError("Network (%s) is not in CIDR presentation format" %
52 network)
53
54
55def no_ip_found_error_out(network):
56 errmsg = ("No IP address found in network(s): %s" % network)
57 raise ValueError(errmsg)
58
59
60def get_address_in_network(network, fallback=None, fatal=False):
61 """Get an IPv4 or IPv6 address within the network from the host.
62
63 :param network (str): CIDR presentation format. For example,
64 '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
65 :param fallback (str): If no address is found, return fallback.
66 :param fatal (boolean): If no address is found, fallback is not
67 set and fatal is True then exit(1).
68 """
69 if network is None:
70 if fallback is not None:
71 return fallback
72
73 if fatal:
74 no_ip_found_error_out(network)
75 else:
76 return None
77
78 networks = network.split() or [network]
79 for network in networks:
80 _validate_cidr(network)
81 network = netaddr.IPNetwork(network)
82 for iface in netifaces.interfaces():
83 addresses = netifaces.ifaddresses(iface)
84 if network.version == 4 and netifaces.AF_INET in addresses:
85 addr = addresses[netifaces.AF_INET][0]['addr']
86 netmask = addresses[netifaces.AF_INET][0]['netmask']
87 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
88 if cidr in network:
89 return str(cidr.ip)
90
91 if network.version == 6 and netifaces.AF_INET6 in addresses:
92 for addr in addresses[netifaces.AF_INET6]:
93 if not addr['addr'].startswith('fe80'):
94 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
95 addr['netmask']))
96 if cidr in network:
97 return str(cidr.ip)
98
99 if fallback is not None:
100 return fallback
101
102 if fatal:
103 no_ip_found_error_out(network)
104
105 return None
106
107
108def is_ipv6(address):
109 """Determine whether provided address is IPv6 or not."""
110 try:
111 address = netaddr.IPAddress(address)
112 except netaddr.AddrFormatError:
113 # probably a hostname - so not an address at all!
114 return False
115
116 return address.version == 6
117
118
119def is_address_in_network(network, address):
120 """
121 Determine whether the provided address is within a network range.
122
123 :param network (str): CIDR presentation format. For example,
124 '192.168.1.0/24'.
125 :param address: An individual IPv4 or IPv6 address without a net
126 mask or subnet prefix. For example, '192.168.1.1'.
127 :returns boolean: Flag indicating whether address is in network.
128 """
129 try:
130 network = netaddr.IPNetwork(network)
131 except (netaddr.core.AddrFormatError, ValueError):
132 raise ValueError("Network (%s) is not in CIDR presentation format" %
133 network)
134
135 try:
136 address = netaddr.IPAddress(address)
137 except (netaddr.core.AddrFormatError, ValueError):
138 raise ValueError("Address (%s) is not in correct presentation format" %
139 address)
140
141 if address in network:
142 return True
143 else:
144 return False
145
146
147def _get_for_address(address, key):
148 """Retrieve an attribute of or the physical interface that
149 the IP address provided could be bound to.
150
151 :param address (str): An individual IPv4 or IPv6 address without a net
152 mask or subnet prefix. For example, '192.168.1.1'.
153 :param key: 'iface' for the physical interface name or an attribute
154 of the configured interface, for example 'netmask'.
155 :returns str: Requested attribute or None if address is not bindable.
156 """
157 address = netaddr.IPAddress(address)
158 for iface in netifaces.interfaces():
159 addresses = netifaces.ifaddresses(iface)
160 if address.version == 4 and netifaces.AF_INET in addresses:
161 addr = addresses[netifaces.AF_INET][0]['addr']
162 netmask = addresses[netifaces.AF_INET][0]['netmask']
163 network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
164 cidr = network.cidr
165 if address in cidr:
166 if key == 'iface':
167 return iface
168 else:
169 return addresses[netifaces.AF_INET][0][key]
170
171 if address.version == 6 and netifaces.AF_INET6 in addresses:
172 for addr in addresses[netifaces.AF_INET6]:
173 if not addr['addr'].startswith('fe80'):
174 network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
175 addr['netmask']))
176 cidr = network.cidr
177 if address in cidr:
178 if key == 'iface':
179 return iface
180 elif key == 'netmask' and cidr:
181 return str(cidr).split('/')[1]
182 else:
183 return addr[key]
184
185 return None
186
187
188get_iface_for_address = partial(_get_for_address, key='iface')
189
190
191get_netmask_for_address = partial(_get_for_address, key='netmask')
192
193
194def format_ipv6_addr(address):
195 """If address is IPv6, wrap it in '[]' otherwise return None.
196
197 This is required by most configuration files when specifying IPv6
198 addresses.
199 """
200 if is_ipv6(address):
201 return "[%s]" % address
202
203 return None
204
205
206def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
207 fatal=True, exc_list=None):
208 """Return the assigned IP address for a given interface, if any."""
209 # Extract nic if passed /dev/ethX
210 if '/' in iface:
211 iface = iface.split('/')[-1]
212
213 if not exc_list:
214 exc_list = []
215
216 try:
217 inet_num = getattr(netifaces, inet_type)
218 except AttributeError:
219 raise Exception("Unknown inet type '%s'" % str(inet_type))
220
221 interfaces = netifaces.interfaces()
222 if inc_aliases:
223 ifaces = []
224 for _iface in interfaces:
225 if iface == _iface or _iface.split(':')[0] == iface:
226 ifaces.append(_iface)
227
228 if fatal and not ifaces:
229 raise Exception("Invalid interface '%s'" % iface)
230
231 ifaces.sort()
232 else:
233 if iface not in interfaces:
234 if fatal:
235 raise Exception("Interface '%s' not found " % (iface))
236 else:
237 return []
238
239 else:
240 ifaces = [iface]
241
242 addresses = []
243 for netiface in ifaces:
244 net_info = netifaces.ifaddresses(netiface)
245 if inet_num in net_info:
246 for entry in net_info[inet_num]:
247 if 'addr' in entry and entry['addr'] not in exc_list:
248 addresses.append(entry['addr'])
249
250 if fatal and not addresses:
251 raise Exception("Interface '%s' doesn't have any %s addresses." %
252 (iface, inet_type))
253
254 return sorted(addresses)
255
256
257get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
258
259
260def get_iface_from_addr(addr):
261 """Work out on which interface the provided address is configured."""
262 for iface in netifaces.interfaces():
263 addresses = netifaces.ifaddresses(iface)
264 for inet_type in addresses:
265 for _addr in addresses[inet_type]:
266 _addr = _addr['addr']
267 # link local
268 ll_key = re.compile("(.+)%.*")
269 raw = re.match(ll_key, _addr)
270 if raw:
271 _addr = raw.group(1)
272
273 if _addr == addr:
274 log("Address '%s' is configured on iface '%s'" %
275 (addr, iface))
276 return iface
277
278 msg = "Unable to infer net iface on which '%s' is configured" % (addr)
279 raise Exception(msg)
280
281
282def sniff_iface(f):
283 """Ensure decorated function is called with a value for iface.
284
285 If no iface provided, inject net iface inferred from unit private address.
286 """
287 def iface_sniffer(*args, **kwargs):
288 if not kwargs.get('iface', None):
289 kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
290
291 return f(*args, **kwargs)
292
293 return iface_sniffer
294
295
296@sniff_iface
297def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
298 dynamic_only=True):
299 """Get assigned IPv6 address for a given interface.
300
301 Returns list of addresses found. If no address found, returns empty list.
302
303 If iface is None, we infer the current primary interface by doing a reverse
304 lookup on the unit private-address.
305
306 We currently only support scope global IPv6 addresses i.e. non-temporary
307 addresses. If no global IPv6 address is found, return the first one found
308 in the ipv6 address list.
309 """
310 addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
311 inc_aliases=inc_aliases, fatal=fatal,
312 exc_list=exc_list)
313
314 if addresses:
315 global_addrs = []
316 for addr in addresses:
317 key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
318 m = re.match(key_scope_link_local, addr)
319 if m:
320 eui_64_mac = m.group(1)
321 iface = m.group(2)
322 else:
323 global_addrs.append(addr)
324
325 if global_addrs:
326 # Make sure any found global addresses are not temporary
327 cmd = ['ip', 'addr', 'show', iface]
328 out = subprocess.check_output(cmd).decode('UTF-8')
329 if dynamic_only:
330 key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
331 else:
332 key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
333
334 addrs = []
335 for line in out.split('\n'):
336 line = line.strip()
337 m = re.match(key, line)
338 if m and 'temporary' not in line:
339 # Return the first valid address we find
340 for addr in global_addrs:
341 if m.group(1) == addr:
342 if not dynamic_only or \
343 m.group(1).endswith(eui_64_mac):
344 addrs.append(addr)
345
346 if addrs:
347 return addrs
348
349 if fatal:
350 raise Exception("Interface '%s' does not have a scope global "
351 "non-temporary ipv6 address." % iface)
352
353 return []
354
355
356def get_bridges(vnic_dir='/sys/devices/virtual/net'):
357 """Return a list of bridges on the system."""
358 b_regex = "%s/*/bridge" % vnic_dir
359 return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
360
361
362def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
363 """Return a list of nics comprising a given bridge on the system."""
364 brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
365 return [x.split('/')[-1] for x in glob.glob(brif_regex)]
366
367
368def is_bridge_member(nic):
369 """Check if a given nic is a member of a bridge."""
370 for bridge in get_bridges():
371 if nic in get_bridge_nics(bridge):
372 return True
373
374 return False
375
376
377def is_ip(address):
378 """
379 Returns True if address is a valid IP address.
380 """
381 try:
382 # Test to see if already an IPv4 address
383 socket.inet_aton(address)
384 return True
385 except socket.error:
386 return False
387
388
389def ns_query(address):
390 try:
391 import dns.resolver
392 except ImportError:
393 apt_install('python-dnspython')
394 import dns.resolver
395
396 if isinstance(address, dns.name.Name):
397 rtype = 'PTR'
398 elif isinstance(address, six.string_types):
399 rtype = 'A'
400 else:
401 return None
402
403 answers = dns.resolver.query(address, rtype)
404 if answers:
405 return str(answers[0])
406 return None
407
408
409def get_host_ip(hostname, fallback=None):
410 """
411 Resolves the IP for a given hostname, or returns
412 the input if it is already an IP.
413 """
414 if is_ip(hostname):
415 return hostname
416
417 ip_addr = ns_query(hostname)
418 if not ip_addr:
419 try:
420 ip_addr = socket.gethostbyname(hostname)
421 except:
422 log("Failed to resolve hostname '%s'" % (hostname),
423 level=WARNING)
424 return fallback
425 return ip_addr
426
427
428def get_hostname(address, fqdn=True):
429 """
430 Resolves hostname for given IP, or returns the input
431 if it is already a hostname.
432 """
433 if is_ip(address):
434 try:
435 import dns.reversename
436 except ImportError:
437 apt_install("python-dnspython")
438 import dns.reversename
439
440 rev = dns.reversename.from_address(address)
441 result = ns_query(rev)
442
443 if not result:
444 try:
445 result = socket.gethostbyaddr(address)[0]
446 except:
447 return None
448 else:
449 result = address
450
451 if fqdn:
452 # strip trailing .
453 if result.endswith('.'):
454 return result[:-1]
455 else:
456 return result
457 else:
458 return result.split('.')[0]
0459
=== added directory 'charmhelpers/contrib/peerstorage'
=== added file 'charmhelpers/contrib/peerstorage/__init__.py'
--- charmhelpers/contrib/peerstorage/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/peerstorage/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,269 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import json
18import six
19
20from charmhelpers.core.hookenv import relation_id as current_relation_id
21from charmhelpers.core.hookenv import (
22 is_relation_made,
23 relation_ids,
24 relation_get as _relation_get,
25 local_unit,
26 relation_set as _relation_set,
27 leader_get as _leader_get,
28 leader_set,
29 is_leader,
30)
31
32
33"""
34This helper provides functions to support use of a peer relation
35for basic key/value storage, with the added benefit that all storage
36can be replicated across peer units.
37
38Requirement to use:
39
40To use this, the "peer_echo()" method has to be called form the peer
41relation's relation-changed hook:
42
43@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
44def cluster_relation_changed():
45 peer_echo()
46
47Once this is done, you can use peer storage from anywhere:
48
49@hooks.hook("some-hook")
50def some_hook():
51 # You can store and retrieve key/values this way:
52 if is_relation_made("cluster"): # from charmhelpers.core.hookenv
53 # There are peers available so we can work with peer storage
54 peer_store("mykey", "myvalue")
55 value = peer_retrieve("mykey")
56 print value
57 else:
58 print "No peers joind the relation, cannot share key/values :("
59"""
60
61
62def leader_get(attribute=None, rid=None):
63 """Wrapper to ensure that settings are migrated from the peer relation.
64
65 This is to support upgrading an environment that does not support
66 Juju leadership election to one that does.
67
68 If a setting is not extant in the leader-get but is on the relation-get
69 peer rel, it is migrated and marked as such so that it is not re-migrated.
70 """
71 migration_key = '__leader_get_migrated_settings__'
72 if not is_leader():
73 return _leader_get(attribute=attribute)
74
75 settings_migrated = False
76 leader_settings = _leader_get(attribute=attribute)
77 previously_migrated = _leader_get(attribute=migration_key)
78
79 if previously_migrated:
80 migrated = set(json.loads(previously_migrated))
81 else:
82 migrated = set([])
83
84 try:
85 if migration_key in leader_settings:
86 del leader_settings[migration_key]
87 except TypeError:
88 pass
89
90 if attribute:
91 if attribute in migrated:
92 return leader_settings
93
94 # If attribute not present in leader db, check if this unit has set
95 # the attribute in the peer relation
96 if not leader_settings:
97 peer_setting = _relation_get(attribute=attribute, unit=local_unit(),
98 rid=rid)
99 if peer_setting:
100 leader_set(settings={attribute: peer_setting})
101 leader_settings = peer_setting
102
103 if leader_settings:
104 settings_migrated = True
105 migrated.add(attribute)
106 else:
107 r_settings = _relation_get(unit=local_unit(), rid=rid)
108 if r_settings:
109 for key in set(r_settings.keys()).difference(migrated):
110 # Leader setting wins
111 if not leader_settings.get(key):
112 leader_settings[key] = r_settings[key]
113
114 settings_migrated = True
115 migrated.add(key)
116
117 if settings_migrated:
118 leader_set(**leader_settings)
119
120 if migrated and settings_migrated:
121 migrated = json.dumps(list(migrated))
122 leader_set(settings={migration_key: migrated})
123
124 return leader_settings
125
126
127def relation_set(relation_id=None, relation_settings=None, **kwargs):
128 """Attempt to use leader-set if supported in the current version of Juju,
129 otherwise falls back on relation-set.
130
131 Note that we only attempt to use leader-set if the provided relation_id is
132 a peer relation id or no relation id is provided (in which case we assume
133 we are within the peer relation context).
134 """
135 try:
136 if relation_id in relation_ids('cluster'):
137 return leader_set(settings=relation_settings, **kwargs)
138 else:
139 raise NotImplementedError
140 except NotImplementedError:
141 return _relation_set(relation_id=relation_id,
142 relation_settings=relation_settings, **kwargs)
143
144
145def relation_get(attribute=None, unit=None, rid=None):
146 """Attempt to use leader-get if supported in the current version of Juju,
147 otherwise falls back on relation-get.
148
149 Note that we only attempt to use leader-get if the provided rid is a peer
150 relation id or no relation id is provided (in which case we assume we are
151 within the peer relation context).
152 """
153 try:
154 if rid in relation_ids('cluster'):
155 return leader_get(attribute, rid)
156 else:
157 raise NotImplementedError
158 except NotImplementedError:
159 return _relation_get(attribute=attribute, rid=rid, unit=unit)
160
161
162def peer_retrieve(key, relation_name='cluster'):
163 """Retrieve a named key from peer relation `relation_name`."""
164 cluster_rels = relation_ids(relation_name)
165 if len(cluster_rels) > 0:
166 cluster_rid = cluster_rels[0]
167 return relation_get(attribute=key, rid=cluster_rid,
168 unit=local_unit())
169 else:
170 raise ValueError('Unable to detect'
171 'peer relation {}'.format(relation_name))
172
173
174def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
175 inc_list=None, exc_list=None):
176 """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
177 inc_list = inc_list if inc_list else []
178 exc_list = exc_list if exc_list else []
179 peerdb_settings = peer_retrieve('-', relation_name=relation_name)
180 matched = {}
181 if peerdb_settings is None:
182 return matched
183 for k, v in peerdb_settings.items():
184 full_prefix = prefix + delimiter
185 if k.startswith(full_prefix):
186 new_key = k.replace(full_prefix, '')
187 if new_key in exc_list:
188 continue
189 if new_key in inc_list or len(inc_list) == 0:
190 matched[new_key] = v
191 return matched
192
193
194def peer_store(key, value, relation_name='cluster'):
195 """Store the key/value pair on the named peer relation `relation_name`."""
196 cluster_rels = relation_ids(relation_name)
197 if len(cluster_rels) > 0:
198 cluster_rid = cluster_rels[0]
199 relation_set(relation_id=cluster_rid,
200 relation_settings={key: value})
201 else:
202 raise ValueError('Unable to detect '
203 'peer relation {}'.format(relation_name))
204
205
206def peer_echo(includes=None, force=False):
207 """Echo filtered attributes back onto the same relation for storage.
208
209 This is a requirement to use the peerstorage module - it needs to be called
210 from the peer relation's changed hook.
211
212 If Juju leader support exists this will be a noop unless force is True.
213 """
214 try:
215 is_leader()
216 except NotImplementedError:
217 pass
218 else:
219 if not force:
220 return # NOOP if leader-election is supported
221
222 # Use original non-leader calls
223 relation_get = _relation_get
224 relation_set = _relation_set
225
226 rdata = relation_get()
227 echo_data = {}
228 if includes is None:
229 echo_data = rdata.copy()
230 for ex in ['private-address', 'public-address']:
231 if ex in echo_data:
232 echo_data.pop(ex)
233 else:
234 for attribute, value in six.iteritems(rdata):
235 for include in includes:
236 if include in attribute:
237 echo_data[attribute] = value
238 if len(echo_data) > 0:
239 relation_set(relation_settings=echo_data)
240
241
242def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
243 peer_store_fatal=False, relation_settings=None,
244 delimiter='_', **kwargs):
245 """Store passed-in arguments both in argument relation and in peer storage.
246
247 It functions like doing relation_set() and peer_store() at the same time,
248 with the same data.
249
250 @param relation_id: the id of the relation to store the data on. Defaults
251 to the current relation.
252 @param peer_store_fatal: Set to True, the function will raise an exception
253 should the peer sotrage not be avialable."""
254
255 relation_settings = relation_settings if relation_settings else {}
256 relation_set(relation_id=relation_id,
257 relation_settings=relation_settings,
258 **kwargs)
259 if is_relation_made(peer_relation_name):
260 for key, value in six.iteritems(dict(list(kwargs.items()) +
261 list(relation_settings.items()))):
262 key_prefix = relation_id or current_relation_id()
263 peer_store(key_prefix + delimiter + key,
264 value,
265 relation_name=peer_relation_name)
266 else:
267 if peer_store_fatal:
268 raise ValueError('Unable to detect '
269 'peer relation {}'.format(peer_relation_name))
0270
=== added directory 'charmhelpers/core'
=== added file 'charmhelpers/core/__init__.py'
--- charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'charmhelpers/core/decorators.py'
--- charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/decorators.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,57 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2014 Canonical Ltd.
19#
20# Authors:
21# Edward Hope-Morley <opentastic@gmail.com>
22#
23
24import time
25
26from charmhelpers.core.hookenv import (
27 log,
28 INFO,
29)
30
31
32def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
33 """If the decorated function raises exception exc_type, allow num_retries
34 retry attempts before raise the exception.
35 """
36 def _retry_on_exception_inner_1(f):
37 def _retry_on_exception_inner_2(*args, **kwargs):
38 retries = num_retries
39 multiplier = 1
40 while True:
41 try:
42 return f(*args, **kwargs)
43 except exc_type:
44 if not retries:
45 raise
46
47 delay = base_delay * multiplier
48 multiplier += 1
49 log("Retrying '%s' %d more times (delay=%s)" %
50 (f.__name__, retries, delay), level=INFO)
51 retries -= 1
52 if delay:
53 time.sleep(delay)
54
55 return _retry_on_exception_inner_2
56
57 return _retry_on_exception_inner_1
058
=== added file 'charmhelpers/core/files.py'
--- charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/files.py 2016-03-10 22:55:31 +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
=== added file 'charmhelpers/core/fstab.py'
--- charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/fstab.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,134 @@
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 io
21import os
22
23__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
24
25
26class Fstab(io.FileIO):
27 """This class extends file in order to implement a file reader/writer
28 for file `/etc/fstab`
29 """
30
31 class Entry(object):
32 """Entry class represents a non-comment line on the `/etc/fstab` file
33 """
34 def __init__(self, device, mountpoint, filesystem,
35 options, d=0, p=0):
36 self.device = device
37 self.mountpoint = mountpoint
38 self.filesystem = filesystem
39
40 if not options:
41 options = "defaults"
42
43 self.options = options
44 self.d = int(d)
45 self.p = int(p)
46
47 def __eq__(self, o):
48 return str(self) == str(o)
49
50 def __str__(self):
51 return "{} {} {} {} {} {}".format(self.device,
52 self.mountpoint,
53 self.filesystem,
54 self.options,
55 self.d,
56 self.p)
57
58 DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
59
60 def __init__(self, path=None):
61 if path:
62 self._path = path
63 else:
64 self._path = self.DEFAULT_PATH
65 super(Fstab, self).__init__(self._path, 'rb+')
66
67 def _hydrate_entry(self, line):
68 # NOTE: use split with no arguments to split on any
69 # whitespace including tabs
70 return Fstab.Entry(*filter(
71 lambda x: x not in ('', None),
72 line.strip("\n").split()))
73
74 @property
75 def entries(self):
76 self.seek(0)
77 for line in self.readlines():
78 line = line.decode('us-ascii')
79 try:
80 if line.strip() and not line.strip().startswith("#"):
81 yield self._hydrate_entry(line)
82 except ValueError:
83 pass
84
85 def get_entry_by_attr(self, attr, value):
86 for entry in self.entries:
87 e_attr = getattr(entry, attr)
88 if e_attr == value:
89 return entry
90 return None
91
92 def add_entry(self, entry):
93 if self.get_entry_by_attr('device', entry.device):
94 return False
95
96 self.write((str(entry) + '\n').encode('us-ascii'))
97 self.truncate()
98 return entry
99
100 def remove_entry(self, entry):
101 self.seek(0)
102
103 lines = [l.decode('us-ascii') for l in self.readlines()]
104
105 found = False
106 for index, line in enumerate(lines):
107 if line.strip() and not line.strip().startswith("#"):
108 if self._hydrate_entry(line) == entry:
109 found = True
110 break
111
112 if not found:
113 return False
114
115 lines.remove(line)
116
117 self.seek(0)
118 self.write(''.join(lines).encode('us-ascii'))
119 self.truncate()
120 return True
121
122 @classmethod
123 def remove_by_mountpoint(cls, mountpoint, path=None):
124 fstab = cls(path=path)
125 entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
126 if entry:
127 return fstab.remove_entry(entry)
128 return False
129
130 @classmethod
131 def add(cls, device, mountpoint, filesystem, options=None, path=None):
132 return cls(path=path).add_entry(Fstab.Entry(device,
133 mountpoint, filesystem,
134 options=options))
0135
=== added file 'charmhelpers/core/hookenv.py'
--- charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/hookenv.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,978 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"Interactions with the Juju environment"
18# Copyright 2013 Canonical Ltd.
19#
20# Authors:
21# Charm Helpers Developers <juju@lists.ubuntu.com>
22
23from __future__ import print_function
24import copy
25from distutils.version import LooseVersion
26from functools import wraps
27import glob
28import os
29import json
30import yaml
31import subprocess
32import sys
33import errno
34import tempfile
35from subprocess import CalledProcessError
36
37import six
38if not six.PY3:
39 from UserDict import UserDict
40else:
41 from collections import UserDict
42
43CRITICAL = "CRITICAL"
44ERROR = "ERROR"
45WARNING = "WARNING"
46INFO = "INFO"
47DEBUG = "DEBUG"
48MARKER = object()
49
50cache = {}
51
52
53def cached(func):
54 """Cache return values for multiple executions of func + args
55
56 For example::
57
58 @cached
59 def unit_get(attribute):
60 pass
61
62 unit_get('test')
63
64 will cache the result of unit_get + 'test' for future calls.
65 """
66 @wraps(func)
67 def wrapper(*args, **kwargs):
68 global cache
69 key = str((func, args, kwargs))
70 try:
71 return cache[key]
72 except KeyError:
73 pass # Drop out of the exception handler scope.
74 res = func(*args, **kwargs)
75 cache[key] = res
76 return res
77 wrapper._wrapped = func
78 return wrapper
79
80
81def flush(key):
82 """Flushes any entries from function cache where the
83 key is found in the function+args """
84 flush_list = []
85 for item in cache:
86 if key in item:
87 flush_list.append(item)
88 for item in flush_list:
89 del cache[item]
90
91
92def log(message, level=None):
93 """Write a message to the juju log"""
94 command = ['juju-log']
95 if level:
96 command += ['-l', level]
97 if not isinstance(message, six.string_types):
98 message = repr(message)
99 command += [message]
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
112
113
114class Serializable(UserDict):
115 """Wrapper, an object that can be serialized to yaml or json"""
116
117 def __init__(self, obj):
118 # wrap the object
119 UserDict.__init__(self)
120 self.data = obj
121
122 def __getattr__(self, attr):
123 # See if this object has attribute.
124 if attr in ("json", "yaml", "data"):
125 return self.__dict__[attr]
126 # Check for attribute in wrapped object.
127 got = getattr(self.data, attr, MARKER)
128 if got is not MARKER:
129 return got
130 # Proxy to the wrapped object via dict interface.
131 try:
132 return self.data[attr]
133 except KeyError:
134 raise AttributeError(attr)
135
136 def __getstate__(self):
137 # Pickle as a standard dictionary.
138 return self.data
139
140 def __setstate__(self, state):
141 # Unpickle into our wrapper.
142 self.data = state
143
144 def json(self):
145 """Serialize the object to json"""
146 return json.dumps(self.data)
147
148 def yaml(self):
149 """Serialize the object to yaml"""
150 return yaml.dump(self.data)
151
152
153def execution_environment():
154 """A convenient bundling of the current execution context"""
155 context = {}
156 context['conf'] = config()
157 if relation_id():
158 context['reltype'] = relation_type()
159 context['relid'] = relation_id()
160 context['rel'] = relation_get()
161 context['unit'] = local_unit()
162 context['rels'] = relations()
163 context['env'] = os.environ
164 return context
165
166
167def in_relation_hook():
168 """Determine whether we're running in a relation hook"""
169 return 'JUJU_RELATION' in os.environ
170
171
172def relation_type():
173 """The scope for the current relation hook"""
174 return os.environ.get('JUJU_RELATION', None)
175
176
177@cached
178def relation_id(relation_name=None, service_or_unit=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')
190
191
192def local_unit():
193 """Local unit ID"""
194 return os.environ['JUJU_UNIT_NAME']
195
196
197def remote_unit():
198 """The remote unit for the current relation hook"""
199 return os.environ.get('JUJU_REMOTE_UNIT', None)
200
201
202def service_name():
203 """The name service group this unit belongs to"""
204 return local_unit().split('/')[0]
205
206
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
218def hook_name():
219 """The name of the currently executing hook"""
220 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
221
222
223class Config(dict):
224 """A dictionary representation of the charm's config.yaml, with some
225 extra features:
226
227 - See which values in the dictionary have changed since the previous hook.
228 - For values that have changed, see what the previous value was.
229 - Store arbitrary data for use in a later hook.
230
231 NOTE: Do not instantiate this object directly - instead call
232 ``hookenv.config()``, which will return an instance of :class:`Config`.
233
234 Example usage::
235
236 >>> # inside a hook
237 >>> from charmhelpers.core import hookenv
238 >>> config = hookenv.config()
239 >>> config['foo']
240 'bar'
241 >>> # store a new key/value for later use
242 >>> config['mykey'] = 'myval'
243
244
245 >>> # user runs `juju set mycharm foo=baz`
246 >>> # now we're inside subsequent config-changed hook
247 >>> config = hookenv.config()
248 >>> config['foo']
249 'baz'
250 >>> # test to see if this val has changed since last hook
251 >>> config.changed('foo')
252 True
253 >>> # what was the previous value?
254 >>> config.previous('foo')
255 'bar'
256 >>> # keys/values that we add are preserved across hooks
257 >>> config['mykey']
258 'myval'
259
260 """
261 CONFIG_FILE_NAME = '.juju-persistent-config'
262
263 def __init__(self, *args, **kw):
264 super(Config, self).__init__(*args, **kw)
265 self.implicit_save = True
266 self._prev_dict = None
267 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
268 if os.path.exists(self.path):
269 self.load_previous()
270 atexit(self._implicit_save)
271
272 def load_previous(self, path=None):
273 """Load previous copy of config from disk.
274
275 In normal usage you don't need to call this method directly - it
276 is called automatically at object initialization.
277
278 :param path:
279
280 File path from which to load the previous config. If `None`,
281 config is loaded from the default location. If `path` is
282 specified, subsequent `save()` calls will write to the same
283 path.
284
285 """
286 self.path = path or self.path
287 with open(self.path) as 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
292
293 def changed(self, key):
294 """Return True if the current value for this key is different from
295 the previous value.
296
297 """
298 if self._prev_dict is None:
299 return True
300 return self.previous(key) != self.get(key)
301
302 def previous(self, key):
303 """Return previous value for this key, or None if there
304 is no previous value.
305
306 """
307 if self._prev_dict:
308 return self._prev_dict.get(key)
309 return None
310
311 def save(self):
312 """Save this config to disk.
313
314 If the charm is using the :mod:`Services Framework <services.base>`
315 or :meth:'@hook <Hooks.hook>' decorator, this
316 is called automatically at the end of successful hook execution.
317 Otherwise, it should be called directly by user code.
318
319 To disable automatic saves, set ``implicit_save=False`` on this
320 instance.
321
322 """
323 with open(self.path, 'w') as f:
324 json.dump(self, f)
325
326 def _implicit_save(self):
327 if self.implicit_save:
328 self.save()
329
330
331@cached
332def config(scope=None):
333 """Juju charm configuration"""
334 config_cmd_line = ['config-get']
335 if scope is not None:
336 config_cmd_line.append(scope)
337 config_cmd_line.append('--format=json')
338 try:
339 config_data = json.loads(
340 subprocess.check_output(config_cmd_line).decode('UTF-8'))
341 if scope is not None:
342 return config_data
343 return Config(config_data)
344 except ValueError:
345 return None
346
347
348@cached
349def relation_get(attribute=None, unit=None, rid=None):
350 """Get relation information"""
351 _args = ['relation-get', '--format=json']
352 if rid:
353 _args.append('-r')
354 _args.append(rid)
355 _args.append(attribute or '-')
356 if unit:
357 _args.append(unit)
358 try:
359 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
360 except ValueError:
361 return None
362 except CalledProcessError as e:
363 if e.returncode == 2:
364 return None
365 raise
366
367
368def relation_set(relation_id=None, relation_settings=None, **kwargs):
369 """Set relation information for the current unit"""
370 relation_settings = relation_settings if relation_settings else {}
371 relation_cmd_line = ['relation-set']
372 accepts_file = "--file" in subprocess.check_output(
373 relation_cmd_line + ["--help"], universal_newlines=True)
374 if relation_id is not None:
375 relation_cmd_line.extend(('-r', relation_id))
376 settings = relation_settings.copy()
377 settings.update(kwargs)
378 for key, value in settings.items():
379 # Force value to be a string: it always should, but some call
380 # sites pass in things like dicts or numbers.
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)
400 # Flush cache of any relation-gets for local unit
401 flush(local_unit())
402
403
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
415@cached
416def relation_ids(reltype=None):
417 """A list of relation_ids"""
418 reltype = reltype or relation_type()
419 relid_cmd_line = ['relation-ids', '--format=json']
420 if reltype is not None:
421 relid_cmd_line.append(reltype)
422 return json.loads(
423 subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
424 return []
425
426
427@cached
428def related_units(relid=None):
429 """A list of related units"""
430 relid = relid or relation_id()
431 units_cmd_line = ['relation-list', '--format=json']
432 if relid is not None:
433 units_cmd_line.extend(('-r', relid))
434 return json.loads(
435 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
436
437
438@cached
439def relation_for_unit(unit=None, rid=None):
440 """Get the json represenation of a unit's relation"""
441 unit = unit or remote_unit()
442 relation = relation_get(unit=unit, rid=rid)
443 for key in relation:
444 if key.endswith('-list'):
445 relation[key] = relation[key].split()
446 relation['__unit__'] = unit
447 return relation
448
449
450@cached
451def relations_for_id(relid=None):
452 """Get relations of a specific relation ID"""
453 relation_data = []
454 relid = relid or relation_ids()
455 for unit in related_units(relid):
456 unit_data = relation_for_unit(unit, relid)
457 unit_data['__relid__'] = relid
458 relation_data.append(unit_data)
459 return relation_data
460
461
462@cached
463def relations_of_type(reltype=None):
464 """Get relations of a specific type"""
465 relation_data = []
466 reltype = reltype or relation_type()
467 for relid in relation_ids(reltype):
468 for relation in relations_for_id(relid):
469 relation['__relid__'] = relid
470 relation_data.append(relation)
471 return relation_data
472
473
474@cached
475def metadata():
476 """Get the current charm metadata.yaml contents as a python object"""
477 with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
478 return yaml.safe_load(md)
479
480
481@cached
482def relation_types():
483 """Get a list of relation types supported by this charm"""
484 rel_types = []
485 md = metadata()
486 for key in ('provides', 'requires', 'peers'):
487 section = md.get(key)
488 if section:
489 rel_types.extend(section.keys())
490 return rel_types
491
492
493@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
564def charm_name():
565 """Get the name of the current charm as is specified on metadata.yaml"""
566 return metadata().get('name')
567
568
569@cached
570def relations():
571 """Get a nested dictionary of relation data for all related units"""
572 rels = {}
573 for reltype in relation_types():
574 relids = {}
575 for relid in relation_ids(reltype):
576 units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
577 for unit in related_units(relid):
578 reldata = relation_get(unit=unit, rid=relid)
579 units[unit] = reldata
580 relids[relid] = units
581 rels[reltype] = relids
582 return rels
583
584
585@cached
586def is_relation_made(relation, keys='private-address'):
587 '''
588 Determine whether a relation is established by checking for
589 presence of key(s). If a list of keys is provided, they
590 must all be present for the relation to be identified as made
591 '''
592 if isinstance(keys, str):
593 keys = [keys]
594 for r_id in relation_ids(relation):
595 for unit in related_units(r_id):
596 context = {}
597 for k in keys:
598 context[k] = relation_get(k, rid=r_id,
599 unit=unit)
600 if None not in context.values():
601 return True
602 return False
603
604
605def open_port(port, protocol="TCP"):
606 """Open a service network port"""
607 _args = ['open-port']
608 _args.append('{}/{}'.format(port, protocol))
609 subprocess.check_call(_args)
610
611
612def close_port(port, protocol="TCP"):
613 """Close a service network port"""
614 _args = ['close-port']
615 _args.append('{}/{}'.format(port, protocol))
616 subprocess.check_call(_args)
617
618
619@cached
620def unit_get(attribute):
621 """Get the unit ID for the remote unit"""
622 _args = ['unit-get', '--format=json', attribute]
623 try:
624 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
625 except ValueError:
626 return None
627
628
629def unit_public_ip():
630 """Get this unit's public IP address"""
631 return unit_get('public-address')
632
633
634def unit_private_ip():
635 """Get this unit's private IP address"""
636 return unit_get('private-address')
637
638
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
671class UnregisteredHookError(Exception):
672 """Raised when an undefined hook is called"""
673 pass
674
675
676class Hooks(object):
677 """A convenient handler for hook functions.
678
679 Example::
680
681 hooks = Hooks()
682
683 # register a hook, taking its name from the function name
684 @hooks.hook()
685 def install():
686 pass # your code here
687
688 # register a hook, providing a custom hook name
689 @hooks.hook("config-changed")
690 def config_changed():
691 pass # your code here
692
693 if __name__ == "__main__":
694 # execute a hook based on the name the program is called by
695 hooks.execute(sys.argv)
696 """
697
698 def __init__(self, config_save=None):
699 super(Hooks, self).__init__()
700 self._hooks = {}
701
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
706
707 def register(self, name, function):
708 """Register a hook"""
709 self._hooks[name] = function
710
711 def execute(self, args):
712 """Execute a registered hook based on args[0]"""
713 _run_atstart()
714 hook_name = os.path.basename(args[0])
715 if hook_name in self._hooks:
716 try:
717 self._hooks[hook_name]()
718 except SystemExit as x:
719 if x.code is None or x.code == 0:
720 _run_atexit()
721 raise
722 _run_atexit()
723 else:
724 raise UnregisteredHookError(hook_name)
725
726 def hook(self, *hook_names):
727 """Decorator, registering them as hooks"""
728 def wrapper(decorated):
729 for hook_name in hook_names:
730 self.register(hook_name, decorated)
731 else:
732 self.register(decorated.__name__, decorated)
733 if '_' in decorated.__name__:
734 self.register(
735 decorated.__name__.replace('_', '-'), decorated)
736 return decorated
737 return wrapper
738
739
740def charm_dir():
741 """Return the root directory of the current charm"""
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@cached
916def juju_version():
917 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
918 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
919 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
920 return subprocess.check_output([jujud, 'version'],
921 universal_newlines=True).strip()
922
923
924@cached
925def has_juju_version(minimum_version):
926 """Return True if the Juju version is at least the provided version"""
927 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
928
929
930_atexit = []
931_atstart = []
932
933
934def atstart(callback, *args, **kwargs):
935 '''Schedule a callback to run before the main hook.
936
937 Callbacks are run in the order they were added.
938
939 This is useful for modules and classes to perform initialization
940 and inject behavior. In particular:
941
942 - Run common code before all of your hooks, such as logging
943 the hook name or interesting relation data.
944 - Defer object or module initialization that requires a hook
945 context until we know there actually is a hook context,
946 making testing easier.
947 - Rather than requiring charm authors to include boilerplate to
948 invoke your helper's behavior, have it run automatically if
949 your object is instantiated or module imported.
950
951 This is not at all useful after your hook framework as been launched.
952 '''
953 global _atstart
954 _atstart.append((callback, args, kwargs))
955
956
957def atexit(callback, *args, **kwargs):
958 '''Schedule a callback to run on successful hook completion.
959
960 Callbacks are run in the reverse order that they were added.'''
961 _atexit.append((callback, args, kwargs))
962
963
964def _run_atstart():
965 '''Hook frameworks must invoke this before running the main hook body.'''
966 global _atstart
967 for callback, args, kwargs in _atstart:
968 callback(*args, **kwargs)
969 del _atstart[:]
970
971
972def _run_atexit():
973 '''Hook frameworks must invoke this after the main hook body has
974 successfully completed. Do not invoke it if the hook fails.'''
975 global _atexit
976 for callback, args, kwargs in reversed(_atexit):
977 callback(*args, **kwargs)
978 del _atexit[:]
0979
=== added file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/host.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,659 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tools for working with the host system"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Nick Moffitt <nick.moffitt@canonical.com>
22# Matthew Wedgwood <matthew.wedgwood@canonical.com>
23
24import os
25import re
26import pwd
27import glob
28import grp
29import random
30import string
31import subprocess
32import hashlib
33from contextlib import contextmanager
34from collections import OrderedDict
35
36import six
37
38from .hookenv import log
39from .fstab import Fstab
40
41
42def service_start(service_name):
43 """Start a system service"""
44 return service('start', service_name)
45
46
47def service_stop(service_name):
48 """Stop a system service"""
49 return service('stop', service_name)
50
51
52def service_restart(service_name):
53 """Restart a system service"""
54 return service('restart', service_name)
55
56
57def service_reload(service_name, restart_on_failure=False):
58 """Reload a system service, optionally falling back to restart if
59 reload fails"""
60 service_result = service('reload', service_name)
61 if not service_result and restart_on_failure:
62 service_result = service('restart', service_name)
63 return service_result
64
65
66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67 """Pause a system service.
68
69 Stop it, and prevent it from starting again at boot."""
70 stopped = True
71 if service_running(service_name):
72 stopped = service_stop(service_name)
73 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 sysv_file = os.path.join(initd_dir, service_name)
75 if init_is_systemd():
76 service('disable', service_name)
77 elif os.path.exists(upstart_file):
78 override_path = os.path.join(
79 init_dir, '{}.override'.format(service_name))
80 with open(override_path, 'w') as fh:
81 fh.write("manual\n")
82 elif os.path.exists(sysv_file):
83 subprocess.check_call(["update-rc.d", service_name, "disable"])
84 else:
85 raise ValueError(
86 "Unable to detect {0} as SystemD, Upstart {1} or"
87 " SysV {2}".format(
88 service_name, upstart_file, sysv_file))
89 return stopped
90
91
92def service_resume(service_name, init_dir="/etc/init",
93 initd_dir="/etc/init.d"):
94 """Resume a system service.
95
96 Reenable starting again at boot. Start the service"""
97 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
98 sysv_file = os.path.join(initd_dir, service_name)
99 if init_is_systemd():
100 service('enable', service_name)
101 elif os.path.exists(upstart_file):
102 override_path = os.path.join(
103 init_dir, '{}.override'.format(service_name))
104 if os.path.exists(override_path):
105 os.unlink(override_path)
106 elif os.path.exists(sysv_file):
107 subprocess.check_call(["update-rc.d", service_name, "enable"])
108 else:
109 raise ValueError(
110 "Unable to detect {0} as SystemD, Upstart {1} or"
111 " SysV {2}".format(
112 service_name, upstart_file, sysv_file))
113
114 started = service_running(service_name)
115 if not started:
116 started = service_start(service_name)
117 return started
118
119
120def service(action, service_name):
121 """Control a system service"""
122 if init_is_systemd():
123 cmd = ['systemctl', action, service_name]
124 else:
125 cmd = ['service', service_name, action]
126 return subprocess.call(cmd) == 0
127
128
129def service_running(service_name):
130 """Determine whether a system service is running"""
131 if init_is_systemd():
132 return service('is-active', service_name)
133 else:
134 try:
135 output = subprocess.check_output(
136 ['service', service_name, 'status'],
137 stderr=subprocess.STDOUT).decode('UTF-8')
138 except subprocess.CalledProcessError:
139 return False
140 else:
141 if ("start/running" in output or "is running" in output or
142 "up and running" in output):
143 return True
144 else:
145 return False
146
147
148def service_available(service_name):
149 """Determine whether a system service is available"""
150 try:
151 subprocess.check_output(
152 ['service', service_name, 'status'],
153 stderr=subprocess.STDOUT).decode('UTF-8')
154 except subprocess.CalledProcessError as e:
155 return b'unrecognized service' not in e.output
156 else:
157 return True
158
159
160SYSTEMD_SYSTEM = '/run/systemd/system'
161
162
163def init_is_systemd():
164 return os.path.isdir(SYSTEMD_SYSTEM)
165
166
167def adduser(username, password=None, shell='/bin/bash', system_user=False,
168 primary_group=None, secondary_groups=None):
169 """
170 Add a user to the system.
171
172 Will log but otherwise succeed if the user already exists.
173
174 :param str username: Username to create
175 :param str password: Password for user; if ``None``, create a system user
176 :param str shell: The default shell for the user
177 :param bool system_user: Whether to create a login or system user
178 :param str primary_group: Primary group for user; defaults to their username
179 :param list secondary_groups: Optional list of additional groups
180
181 :returns: The password database entry struct, as returned by `pwd.getpwnam`
182 """
183 try:
184 user_info = pwd.getpwnam(username)
185 log('user {0} already exists!'.format(username))
186 except KeyError:
187 log('creating user {0}'.format(username))
188 cmd = ['useradd']
189 if system_user or password is None:
190 cmd.append('--system')
191 else:
192 cmd.extend([
193 '--create-home',
194 '--shell', shell,
195 '--password', password,
196 ])
197 if not primary_group:
198 try:
199 grp.getgrnam(username)
200 primary_group = username # avoid "group exists" error
201 except KeyError:
202 pass
203 if primary_group:
204 cmd.extend(['-g', primary_group])
205 if secondary_groups:
206 cmd.extend(['-G', ','.join(secondary_groups)])
207 cmd.append(username)
208 subprocess.check_call(cmd)
209 user_info = pwd.getpwnam(username)
210 return user_info
211
212
213def user_exists(username):
214 """Check if a user exists"""
215 try:
216 pwd.getpwnam(username)
217 user_exists = True
218 except KeyError:
219 user_exists = False
220 return user_exists
221
222
223def add_group(group_name, system_group=False):
224 """Add a group to the system"""
225 try:
226 group_info = grp.getgrnam(group_name)
227 log('group {0} already exists!'.format(group_name))
228 except KeyError:
229 log('creating group {0}'.format(group_name))
230 cmd = ['addgroup']
231 if system_group:
232 cmd.append('--system')
233 else:
234 cmd.extend([
235 '--group',
236 ])
237 cmd.append(group_name)
238 subprocess.check_call(cmd)
239 group_info = grp.getgrnam(group_name)
240 return group_info
241
242
243def add_user_to_group(username, group):
244 """Add a user to a group"""
245 cmd = ['gpasswd', '-a', username, group]
246 log("Adding user {} to group {}".format(username, group))
247 subprocess.check_call(cmd)
248
249
250def rsync(from_path, to_path, flags='-r', options=None):
251 """Replicate the contents of a path"""
252 options = options or ['--delete', '--executability']
253 cmd = ['/usr/bin/rsync', flags]
254 cmd.extend(options)
255 cmd.append(from_path)
256 cmd.append(to_path)
257 log(" ".join(cmd))
258 return subprocess.check_output(cmd).decode('UTF-8').strip()
259
260
261def symlink(source, destination):
262 """Create a symbolic link"""
263 log("Symlinking {} as {}".format(source, destination))
264 cmd = [
265 'ln',
266 '-sf',
267 source,
268 destination,
269 ]
270 subprocess.check_call(cmd)
271
272
273def mkdir(path, owner='root', group='root', perms=0o555, force=False):
274 """Create a directory"""
275 log("Making dir {} {}:{} {:o}".format(path, owner, group,
276 perms))
277 uid = pwd.getpwnam(owner).pw_uid
278 gid = grp.getgrnam(group).gr_gid
279 realpath = os.path.abspath(path)
280 path_exists = os.path.exists(realpath)
281 if path_exists and force:
282 if not os.path.isdir(realpath):
283 log("Removing non-directory file {} prior to mkdir()".format(path))
284 os.unlink(realpath)
285 os.makedirs(realpath, perms)
286 elif not path_exists:
287 os.makedirs(realpath, perms)
288 os.chown(realpath, uid, gid)
289 os.chmod(realpath, perms)
290
291
292def write_file(path, content, owner='root', group='root', perms=0o444):
293 """Create or overwrite a file with the contents of a byte string."""
294 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
295 uid = pwd.getpwnam(owner).pw_uid
296 gid = grp.getgrnam(group).gr_gid
297 with open(path, 'wb') as target:
298 os.fchown(target.fileno(), uid, gid)
299 os.fchmod(target.fileno(), perms)
300 target.write(content)
301
302
303def fstab_remove(mp):
304 """Remove the given mountpoint entry from /etc/fstab
305 """
306 return Fstab.remove_by_mountpoint(mp)
307
308
309def fstab_add(dev, mp, fs, options=None):
310 """Adds the given device entry to the /etc/fstab file
311 """
312 return Fstab.add(dev, mp, fs, options=options)
313
314
315def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
316 """Mount a filesystem at a particular mountpoint"""
317 cmd_args = ['mount']
318 if options is not None:
319 cmd_args.extend(['-o', options])
320 cmd_args.extend([device, mountpoint])
321 try:
322 subprocess.check_output(cmd_args)
323 except subprocess.CalledProcessError as e:
324 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
325 return False
326
327 if persist:
328 return fstab_add(device, mountpoint, filesystem, options=options)
329 return True
330
331
332def umount(mountpoint, persist=False):
333 """Unmount a filesystem"""
334 cmd_args = ['umount', mountpoint]
335 try:
336 subprocess.check_output(cmd_args)
337 except subprocess.CalledProcessError as e:
338 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
339 return False
340
341 if persist:
342 return fstab_remove(mountpoint)
343 return True
344
345
346def mounts():
347 """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
348 with open('/proc/mounts') as f:
349 # [['/mount/point','/dev/path'],[...]]
350 system_mounts = [m[1::-1] for m in [l.strip().split()
351 for l in f.readlines()]]
352 return system_mounts
353
354
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
366def file_hash(path, hash_type='md5'):
367 """
368 Generate a hash checksum of the contents of 'path' or None if not found.
369
370 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
371 such as md5, sha1, sha256, sha512, etc.
372 """
373 if os.path.exists(path):
374 h = getattr(hashlib, hash_type)()
375 with open(path, 'rb') as source:
376 h.update(source.read())
377 return h.hexdigest()
378 else:
379 return None
380
381
382def path_hash(path):
383 """
384 Generate a hash checksum of all files matching 'path'. Standard wildcards
385 like '*' and '?' are supported, see documentation for the 'glob' module for
386 more information.
387
388 :return: dict: A { filename: hash } dictionary for all matched files.
389 Empty if none found.
390 """
391 return {
392 filename: file_hash(filename)
393 for filename in glob.iglob(path)
394 }
395
396
397def check_hash(path, checksum, hash_type='md5'):
398 """
399 Validate a file using a cryptographic checksum.
400
401 :param str checksum: Value of the checksum used to validate the file.
402 :param str hash_type: Hash algorithm used to generate `checksum`.
403 Can be any hash alrgorithm supported by :mod:`hashlib`,
404 such as md5, sha1, sha256, sha512, etc.
405 :raises ChecksumError: If the file fails the checksum
406
407 """
408 actual_checksum = file_hash(path, hash_type)
409 if checksum != actual_checksum:
410 raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
411
412
413class ChecksumError(ValueError):
414 pass
415
416
417def restart_on_change(restart_map, stopstart=False):
418 """Restart services based on configuration files changing
419
420 This function is used a decorator, for example::
421
422 @restart_on_change({
423 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
424 '/etc/apache/sites-enabled/*': [ 'apache2' ]
425 })
426 def config_changed():
427 pass # your code here
428
429 In this example, the cinder-api and cinder-volume services
430 would be restarted if /etc/ceph/ceph.conf is changed by the
431 ceph_client_changed function. The apache2 service would be
432 restarted if any file matching the pattern got changed, created
433 or removed. Standard wildcards are supported, see documentation
434 for the 'glob' module for more information.
435 """
436 def wrap(f):
437 def wrapped_f(*args, **kwargs):
438 checksums = {path: path_hash(path) for path in restart_map}
439 f(*args, **kwargs)
440 restarts = []
441 for path in restart_map:
442 if path_hash(path) != checksums[path]:
443 restarts += restart_map[path]
444 services_list = list(OrderedDict.fromkeys(restarts))
445 if not stopstart:
446 for service_name in services_list:
447 service('restart', service_name)
448 else:
449 for action in ['stop', 'start']:
450 for service_name in services_list:
451 service(action, service_name)
452 return wrapped_f
453 return wrap
454
455
456def lsb_release():
457 """Return /etc/lsb-release in a dict"""
458 d = {}
459 with open('/etc/lsb-release', 'r') as lsb:
460 for l in lsb:
461 k, v = l.split('=')
462 d[k.strip()] = v.strip()
463 return d
464
465
466def pwgen(length=None):
467 """Generate a random pasword."""
468 if length is None:
469 # A random length is ok to use a weak PRNG
470 length = random.choice(range(35, 45))
471 alphanumeric_chars = [
472 l for l in (string.ascii_letters + string.digits)
473 if l not in 'l0QD1vAEIOUaeiou']
474 # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
475 # actual password
476 random_generator = random.SystemRandom()
477 random_chars = [
478 random_generator.choice(alphanumeric_chars) for _ in range(length)]
479 return(''.join(random_chars))
480
481
482def is_phy_iface(interface):
483 """Returns True if interface is not virtual, otherwise False."""
484 if interface:
485 sys_net = '/sys/class/net'
486 if os.path.isdir(sys_net):
487 for iface in glob.glob(os.path.join(sys_net, '*')):
488 if '/virtual/' in os.path.realpath(iface):
489 continue
490
491 if interface == os.path.basename(iface):
492 return True
493
494 return False
495
496
497def get_bond_master(interface):
498 """Returns bond master if interface is bond slave otherwise None.
499
500 NOTE: the provided interface is expected to be physical
501 """
502 if interface:
503 iface_path = '/sys/class/net/%s' % (interface)
504 if os.path.exists(iface_path):
505 if '/virtual/' in os.path.realpath(iface_path):
506 return None
507
508 master = os.path.join(iface_path, 'master')
509 if os.path.exists(master):
510 master = os.path.realpath(master)
511 # make sure it is a bond master
512 if os.path.exists(os.path.join(master, 'bonding')):
513 return os.path.basename(master)
514
515 return None
516
517
518def list_nics(nic_type=None):
519 '''Return a list of nics of given type(s)'''
520 if isinstance(nic_type, six.string_types):
521 int_types = [nic_type]
522 else:
523 int_types = nic_type
524
525 interfaces = []
526 if nic_type:
527 for int_type in int_types:
528 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
529 ip_output = subprocess.check_output(cmd).decode('UTF-8')
530 ip_output = ip_output.split('\n')
531 ip_output = (line for line in ip_output if line)
532 for line in ip_output:
533 if line.split()[1].startswith(int_type):
534 matched = re.search('.*: (' + int_type +
535 r'[0-9]+\.[0-9]+)@.*', line)
536 if matched:
537 iface = matched.groups()[0]
538 else:
539 iface = line.split()[1].replace(":", "")
540
541 if iface not in interfaces:
542 interfaces.append(iface)
543 else:
544 cmd = ['ip', 'a']
545 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
546 ip_output = (line.strip() for line in ip_output if line)
547
548 key = re.compile('^[0-9]+:\s+(.+):')
549 for line in ip_output:
550 matched = re.search(key, line)
551 if matched:
552 iface = matched.group(1)
553 iface = iface.partition("@")[0]
554 if iface not in interfaces:
555 interfaces.append(iface)
556
557 return interfaces
558
559
560def set_nic_mtu(nic, mtu):
561 '''Set MTU on a network interface'''
562 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
563 subprocess.check_call(cmd)
564
565
566def get_nic_mtu(nic):
567 cmd = ['ip', 'addr', 'show', nic]
568 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
569 mtu = ""
570 for line in ip_output:
571 words = line.split()
572 if 'mtu' in words:
573 mtu = words[words.index("mtu") + 1]
574 return mtu
575
576
577def get_nic_hwaddr(nic):
578 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
579 ip_output = subprocess.check_output(cmd).decode('UTF-8')
580 hwaddr = ""
581 words = ip_output.split()
582 if 'link/ether' in words:
583 hwaddr = words[words.index('link/ether') + 1]
584 return hwaddr
585
586
587def cmp_pkgrevno(package, revno, pkgcache=None):
588 '''Compare supplied revno with the revno of the installed package
589
590 * 1 => Installed revno is greater than supplied arg
591 * 0 => Installed revno is the same as supplied arg
592 * -1 => Installed revno is less than supplied arg
593
594 This function imports apt_cache function from charmhelpers.fetch if
595 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
596 you call this function, or pass an apt_pkg.Cache() instance.
597 '''
598 import apt_pkg
599 if not pkgcache:
600 from charmhelpers.fetch import apt_cache
601 pkgcache = apt_cache()
602 pkg = pkgcache[package]
603 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
604
605
606@contextmanager
607def chdir(d):
608 cur = os.getcwd()
609 try:
610 yield os.chdir(d)
611 finally:
612 os.chdir(cur)
613
614
615def chownr(path, owner, group, follow_links=True, chowntopdir=False):
616 """
617 Recursively change user and group ownership of files and directories
618 in given path. Doesn't chown path itself by default, only its children.
619
620 :param bool follow_links: Also Chown links if True
621 :param bool chowntopdir: Also chown path itself if True
622 """
623 uid = pwd.getpwnam(owner).pw_uid
624 gid = grp.getgrnam(group).gr_gid
625 if follow_links:
626 chown = os.chown
627 else:
628 chown = os.lchown
629
630 if chowntopdir:
631 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
632 if not broken_symlink:
633 chown(path, uid, gid)
634 for root, dirs, files in os.walk(path):
635 for name in dirs + files:
636 full = os.path.join(root, name)
637 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
638 if not broken_symlink:
639 chown(full, uid, gid)
640
641
642def lchownr(path, owner, group):
643 chownr(path, owner, group, follow_links=False)
644
645
646def get_total_ram():
647 '''The total amount of system RAM in bytes.
648
649 This is what is reported by the OS, and may be overcommitted when
650 there are multiple containers hosted on the same machine.
651 '''
652 with open('/proc/meminfo', 'r') as f:
653 for line in f.readlines():
654 if line:
655 key, value, unit = line.split()
656 if key == 'MemTotal:':
657 assert unit == 'kB', 'Unknown unit'
658 return int(value) * 1024 # Classic, not KiB.
659 raise NotImplementedError()
0660
=== added file 'charmhelpers/core/hugepage.py'
--- charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/hugepage.py 2016-03-10 22:55:31 +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 'charmhelpers/core/kernel.py'
--- charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/kernel.py 2016-03-10 22:55:31 +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
=== added directory 'charmhelpers/core/services'
=== added file 'charmhelpers/core/services/__init__.py'
--- charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/services/__init__.py 2016-03-10 22:55:31 +0000
@@ -0,0 +1,18 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from .base import * # NOQA
18from .helpers import * # NOQA
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: