Merge lp:~bac/charms/trusty/openstack-dashboard/dashboard-plugin into lp:~yellow/charms/trusty/openstack-dashboard/dashboard-plugin

Proposed by Brad Crittenden
Status: Merged
Merged at revision: 87
Proposed branch: lp:~bac/charms/trusty/openstack-dashboard/dashboard-plugin
Merge into: lp:~yellow/charms/trusty/openstack-dashboard/dashboard-plugin
Diff against target: 6515 lines (+3109/-999)
60 files modified
Makefile (+1/-0)
actions/openstack_upgrade.py (+0/-34)
charm-helpers-hooks.yaml (+1/-1)
charm-helpers-tests.yaml (+1/-1)
config.yaml (+24/-0)
hooks/charmhelpers/cli/__init__.py (+0/-191)
hooks/charmhelpers/cli/benchmark.py (+0/-36)
hooks/charmhelpers/cli/commands.py (+0/-32)
hooks/charmhelpers/cli/hookenv.py (+0/-23)
hooks/charmhelpers/cli/host.py (+0/-31)
hooks/charmhelpers/cli/unitdata.py (+0/-39)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+52/-14)
hooks/charmhelpers/contrib/network/ip.py (+26/-22)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+130/-12)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+381/-0)
hooks/charmhelpers/contrib/openstack/context.py (+132/-31)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+7/-5)
hooks/charmhelpers/contrib/openstack/neutron.py (+19/-5)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+6/-0)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+19/-11)
hooks/charmhelpers/contrib/openstack/templating.py (+30/-2)
hooks/charmhelpers/contrib/openstack/utils.py (+240/-27)
hooks/charmhelpers/contrib/python/packages.py (+13/-4)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+656/-61)
hooks/charmhelpers/contrib/storage/linux/loopback.py (+10/-0)
hooks/charmhelpers/core/files.py (+0/-45)
hooks/charmhelpers/core/hookenv.py (+84/-4)
hooks/charmhelpers/core/host.py (+120/-32)
hooks/charmhelpers/core/hugepage.py (+0/-62)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/helpers.py (+14/-5)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/charmhelpers/core/templating.py (+21/-8)
hooks/charmhelpers/fetch/__init__.py (+10/-2)
hooks/charmhelpers/fetch/archiveurl.py (+1/-1)
hooks/charmhelpers/fetch/bzrurl.py (+22/-32)
hooks/charmhelpers/fetch/giturl.py (+19/-24)
hooks/charmhelpers/payload/archive.py (+73/-0)
hooks/horizon_hooks.py (+13/-4)
hooks/horizon_utils.py (+3/-0)
metadata.yaml (+2/-2)
templates/icehouse/local_settings.py (+3/-3)
templates/juno/local_settings.py (+3/-3)
tests/018-basic-trusty-liberty (+11/-0)
tests/019-basic-trusty-mitaka (+11/-0)
tests/020-basic-wily-liberty (+9/-0)
tests/021-basic-xenial-mitaka (+9/-0)
tests/052-basic-trusty-kilo-git (+0/-12)
tests/README (+96/-46)
tests/basic_deployment.py (+1/-1)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+193/-19)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+130/-12)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+381/-0)
tests/setup/00-setup (+17/-0)
tests/tests.yaml (+0/-20)
unit_tests/test_actions_openstack_upgrade.py (+0/-53)
unit_tests/test_horizon_contexts.py (+2/-2)
unit_tests/test_horizon_hooks.py (+10/-22)
unit_tests/test_horizon_utils.py (+1/-1)
To merge this branch: bzr merge lp:~bac/charms/trusty/openstack-dashboard/dashboard-plugin
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+286514@code.launchpad.net

Description of the change

Merge upstream

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2015-10-06 15:06:44 +0000
+++ Makefile 2016-02-18 14:28:13 +0000
@@ -13,6 +13,7 @@
1313
14functional_test:14functional_test:
15 @echo Starting Amulet tests...15 @echo Starting Amulet tests...
16 @tests/setup/00-setup
16 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 270017 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
1718
18bin/charm_helpers_sync.py:19bin/charm_helpers_sync.py:
1920
=== added symlink 'actions/openstack-upgrade'
=== target is u'openstack_upgrade.py'
=== removed symlink 'actions/openstack-upgrade'
=== target was u'openstack_upgrade.py'
=== added file 'actions/openstack_upgrade.py'
--- actions/openstack_upgrade.py 1970-01-01 00:00:00 +0000
+++ actions/openstack_upgrade.py 2016-02-18 14:28:13 +0000
@@ -0,0 +1,34 @@
1#!/usr/bin/python
2import sys
3
4sys.path.append('hooks/')
5
6from charmhelpers.contrib.openstack.utils import (
7 do_action_openstack_upgrade,
8)
9
10from horizon_utils import (
11 do_openstack_upgrade,
12)
13
14from horizon_hooks import (
15 config_changed,
16 CONFIGS,
17)
18
19
20def openstack_upgrade():
21 """Upgrade packages to config-set Openstack version.
22
23 If the charm was installed from source we cannot upgrade it.
24 For backwards compatibility a config flag must be set for this
25 code to run, otherwise a full service level upgrade will fire
26 on config-changed."""
27
28 if do_action_openstack_upgrade('openstack-dashboard',
29 do_openstack_upgrade,
30 CONFIGS):
31 config_changed()
32
33if __name__ == '__main__':
34 openstack_upgrade()
035
=== removed file 'actions/openstack_upgrade.py'
--- actions/openstack_upgrade.py 2015-09-23 14:37:57 +0000
+++ actions/openstack_upgrade.py 1970-01-01 00:00:00 +0000
@@ -1,34 +0,0 @@
1#!/usr/bin/python
2import sys
3
4sys.path.append('hooks/')
5
6from charmhelpers.contrib.openstack.utils import (
7 do_action_openstack_upgrade,
8)
9
10from horizon_utils import (
11 do_openstack_upgrade,
12)
13
14from horizon_hooks import (
15 config_changed,
16 CONFIGS,
17)
18
19
20def openstack_upgrade():
21 """Upgrade packages to config-set Openstack version.
22
23 If the charm was installed from source we cannot upgrade it.
24 For backwards compatibility a config flag must be set for this
25 code to run, otherwise a full service level upgrade will fire
26 on config-changed."""
27
28 if do_action_openstack_upgrade('openstack-dashboard',
29 do_openstack_upgrade,
30 CONFIGS):
31 config_changed()
32
33if __name__ == '__main__':
34 openstack_upgrade()
350
=== modified file 'charm-helpers-hooks.yaml'
--- charm-helpers-hooks.yaml 2015-07-31 13:11:17 +0000
+++ charm-helpers-hooks.yaml 2016-02-18 14:28:13 +0000
@@ -1,4 +1,4 @@
1branch: lp:charm-helpers1branch: lp:~openstack-charmers/charm-helpers/stable
2destination: hooks/charmhelpers2destination: hooks/charmhelpers
3include:3include:
4 - core4 - core
55
=== modified file 'charm-helpers-tests.yaml'
--- charm-helpers-tests.yaml 2015-02-10 18:50:39 +0000
+++ charm-helpers-tests.yaml 2016-02-18 14:28:13 +0000
@@ -1,4 +1,4 @@
1branch: lp:charm-helpers1branch: lp:~openstack-charmers/charm-helpers/stable
2destination: tests/charmhelpers2destination: tests/charmhelpers
3include:3include:
4 - contrib.amulet4 - contrib.amulet
55
=== modified file 'config.yaml'
--- config.yaml 2015-10-08 20:29:11 +0000
+++ config.yaml 2016-02-18 14:28:13 +0000
@@ -193,6 +193,30 @@
193 wait for you to execute the openstack-upgrade action for this charm on193 wait for you to execute the openstack-upgrade action for this charm on
194 each unit. If False it will revert to existing behavior of upgrading194 each unit. If False it will revert to existing behavior of upgrading
195 all units on config change.195 all units on config change.
196 haproxy-server-timeout:
197 type: int
198 default:
199 description: |
200 Server timeout configuration in ms for haproxy, used in HA
201 configurations. If not provided, default value of 30000ms is used.
202 haproxy-client-timeout:
203 type: int
204 default:
205 description: |
206 Client timeout configuration in ms for haproxy, used in HA
207 configurations. If not provided, default value of 30000ms is used.
208 haproxy-queue-timeout:
209 type: int
210 default:
211 description: |
212 Queue timeout configuration in ms for haproxy, used in HA
213 configurations. If not provided, default value of 5000ms is used.
214 haproxy-connect-timeout:
215 type: int
216 default:
217 description: |
218 Connect timeout configuration in ms for haproxy, used in HA
219 configurations. If not provided, default value of 5000ms is used.
196 apache_http_addendum:220 apache_http_addendum:
197 type: string221 type: string
198 default: ''222 default: ''
199223
=== added directory 'hooks/charmhelpers/cli'
=== removed directory 'hooks/charmhelpers/cli'
=== added file 'hooks/charmhelpers/cli/__init__.py'
--- hooks/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/cli/__init__.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/cli/__init__.py'
--- hooks/charmhelpers/cli/__init__.py 2015-08-18 17:34:36 +0000
+++ hooks/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,191 +0,0 @@
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
23from charmhelpers.core import 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 unitdata._KV:
167 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': '*'}
1920
=== added file 'hooks/charmhelpers/cli/benchmark.py'
--- hooks/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/cli/benchmark.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/cli/benchmark.py'
--- hooks/charmhelpers/cli/benchmark.py 2015-07-31 13:11:17 +0000
+++ hooks/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
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
370
=== added file 'hooks/charmhelpers/cli/commands.py'
--- hooks/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/cli/commands.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/cli/commands.py'
--- hooks/charmhelpers/cli/commands.py 2015-08-18 17:34:36 +0000
+++ hooks/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
@@ -1,32 +0,0 @@
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
330
=== added file 'hooks/charmhelpers/cli/hookenv.py'
--- hooks/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/cli/hookenv.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/cli/hookenv.py'
--- hooks/charmhelpers/cli/hookenv.py 2015-08-18 17:34:36 +0000
+++ hooks/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
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)
240
=== added file 'hooks/charmhelpers/cli/host.py'
--- hooks/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/cli/host.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/cli/host.py'
--- hooks/charmhelpers/cli/host.py 2015-07-31 13:11:17 +0000
+++ hooks/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
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
320
=== added file 'hooks/charmhelpers/cli/unitdata.py'
--- hooks/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/cli/unitdata.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/cli/unitdata.py'
--- hooks/charmhelpers/cli/unitdata.py 2015-07-31 13:11:17 +0000
+++ hooks/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
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
400
=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-04-19 09:02:03 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-02-18 14:28:13 +0000
@@ -148,6 +148,13 @@
148 self.description = description148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)149 self.check_cmd = self._locate_cmd(check_cmd)
150150
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
151 def _locate_cmd(self, check_cmd):158 def _locate_cmd(self, check_cmd):
152 search_path = (159 search_path = (
153 '/usr/lib/nagios/plugins',160 '/usr/lib/nagios/plugins',
@@ -163,9 +170,21 @@
163 log('Check command not found: {}'.format(parts[0]))170 log('Check command not found: {}'.format(parts[0]))
164 return ''171 return ''
165172
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
166 def write(self, nagios_context, hostname, nagios_servicegroups):186 def write(self, nagios_context, hostname, nagios_servicegroups):
167 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(187 nrpe_check_file = self._get_check_filename()
168 self.command)
169 with open(nrpe_check_file, 'w') as nrpe_check_config:188 with open(nrpe_check_file, 'w') as nrpe_check_config:
170 nrpe_check_config.write("# check {}\n".format(self.shortname))189 nrpe_check_config.write("# check {}\n".format(self.shortname))
171 nrpe_check_config.write("command[{}]={}\n".format(190 nrpe_check_config.write("command[{}]={}\n".format(
@@ -180,9 +199,7 @@
180199
181 def write_service_config(self, nagios_context, hostname,200 def write_service_config(self, nagios_context, hostname,
182 nagios_servicegroups):201 nagios_servicegroups):
183 for f in os.listdir(NRPE.nagios_exportdir):202 self._remove_service_files()
184 if re.search('.*{}.cfg'.format(self.command), f):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186203
187 templ_vars = {204 templ_vars = {
188 'nagios_hostname': hostname,205 'nagios_hostname': hostname,
@@ -192,8 +209,7 @@
192 'command': self.command,209 'command': self.command,
193 }210 }
194 nrpe_service_text = Check.service_template.format(**templ_vars)211 nrpe_service_text = Check.service_template.format(**templ_vars)
195 nrpe_service_file = '{}/service__{}_{}.cfg'.format(212 nrpe_service_file = self._get_service_filename(hostname)
196 NRPE.nagios_exportdir, hostname, self.command)
197 with open(nrpe_service_file, 'w') as nrpe_service_config:213 with open(nrpe_service_file, 'w') as nrpe_service_config:
198 nrpe_service_config.write(str(nrpe_service_text))214 nrpe_service_config.write(str(nrpe_service_text))
199215
@@ -218,12 +234,32 @@
218 if hostname:234 if hostname:
219 self.hostname = hostname235 self.hostname = hostname
220 else:236 else:
221 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)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)
222 self.checks = []242 self.checks = []
223243
224 def add_check(self, *args, **kwargs):244 def add_check(self, *args, **kwargs):
225 self.checks.append(Check(*args, **kwargs))245 self.checks.append(Check(*args, **kwargs))
226246
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
227 def write(self):263 def write(self):
228 try:264 try:
229 nagios_uid = pwd.getpwnam('nagios').pw_uid265 nagios_uid = pwd.getpwnam('nagios').pw_uid
@@ -260,7 +296,7 @@
260 :param str relation_name: Name of relation nrpe sub joined to296 :param str relation_name: Name of relation nrpe sub joined to
261 """297 """
262 for rel in relations_of_type(relation_name):298 for rel in relations_of_type(relation_name):
263 if 'nagios_hostname' in rel:299 if 'nagios_host_context' in rel:
264 return rel['nagios_host_context']300 return rel['nagios_host_context']
265301
266302
@@ -301,11 +337,13 @@
301 upstart_init = '/etc/init/%s.conf' % svc337 upstart_init = '/etc/init/%s.conf' % svc
302 sysv_init = '/etc/init.d/%s' % svc338 sysv_init = '/etc/init.d/%s' % svc
303 if os.path.exists(upstart_init):339 if os.path.exists(upstart_init):
304 nrpe.add_check(340 # Don't add a check for these services from neutron-gateway
305 shortname=svc,341 if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
306 description='process check {%s}' % unit_name,342 nrpe.add_check(
307 check_cmd='check_upstart_job %s' % svc343 shortname=svc,
308 )344 description='process check {%s}' % unit_name,
345 check_cmd='check_upstart_job %s' % svc
346 )
309 elif os.path.exists(sysv_init):347 elif os.path.exists(sysv_init):
310 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc348 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
311 cron_file = ('*/5 * * * * root '349 cron_file = ('*/5 * * * * root '
312350
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:42:35 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2016-02-18 14:28:13 +0000
@@ -23,7 +23,7 @@
23from functools import partial23from functools import partial
2424
25from charmhelpers.core.hookenv import unit_get25from charmhelpers.core.hookenv import unit_get
26from charmhelpers.fetch import apt_install26from charmhelpers.fetch import apt_install, apt_update
27from charmhelpers.core.hookenv import (27from charmhelpers.core.hookenv import (
28 log,28 log,
29 WARNING,29 WARNING,
@@ -32,13 +32,15 @@
32try:32try:
33 import netifaces33 import netifaces
34except ImportError:34except ImportError:
35 apt_install('python-netifaces')35 apt_update(fatal=True)
36 apt_install('python-netifaces', fatal=True)
36 import netifaces37 import netifaces
3738
38try:39try:
39 import netaddr40 import netaddr
40except ImportError:41except ImportError:
41 apt_install('python-netaddr')42 apt_update(fatal=True)
43 apt_install('python-netaddr', fatal=True)
42 import netaddr44 import netaddr
4345
4446
@@ -51,7 +53,7 @@
5153
5254
53def no_ip_found_error_out(network):55def no_ip_found_error_out(network):
54 errmsg = ("No IP address found in network: %s" % network)56 errmsg = ("No IP address found in network(s): %s" % network)
55 raise ValueError(errmsg)57 raise ValueError(errmsg)
5658
5759
@@ -59,7 +61,7 @@
59 """Get an IPv4 or IPv6 address within the network from the host.61 """Get an IPv4 or IPv6 address within the network from the host.
6062
61 :param network (str): CIDR presentation format. For example,63 :param network (str): CIDR presentation format. For example,
62 '192.168.1.0/24'.64 '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
63 :param fallback (str): If no address is found, return fallback.65 :param fallback (str): If no address is found, return fallback.
64 :param fatal (boolean): If no address is found, fallback is not66 :param fatal (boolean): If no address is found, fallback is not
65 set and fatal is True then exit(1).67 set and fatal is True then exit(1).
@@ -73,24 +75,26 @@
73 else:75 else:
74 return None76 return None
7577
76 _validate_cidr(network)78 networks = network.split() or [network]
77 network = netaddr.IPNetwork(network)79 for network in networks:
78 for iface in netifaces.interfaces():80 _validate_cidr(network)
79 addresses = netifaces.ifaddresses(iface)81 network = netaddr.IPNetwork(network)
80 if network.version == 4 and netifaces.AF_INET in addresses:82 for iface in netifaces.interfaces():
81 addr = addresses[netifaces.AF_INET][0]['addr']83 addresses = netifaces.ifaddresses(iface)
82 netmask = addresses[netifaces.AF_INET][0]['netmask']84 if network.version == 4 and netifaces.AF_INET in addresses:
83 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))85 addr = addresses[netifaces.AF_INET][0]['addr']
84 if cidr in network:86 netmask = addresses[netifaces.AF_INET][0]['netmask']
85 return str(cidr.ip)87 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
88 if cidr in network:
89 return str(cidr.ip)
8690
87 if network.version == 6 and netifaces.AF_INET6 in addresses:91 if network.version == 6 and netifaces.AF_INET6 in addresses:
88 for addr in addresses[netifaces.AF_INET6]:92 for addr in addresses[netifaces.AF_INET6]:
89 if not addr['addr'].startswith('fe80'):93 if not addr['addr'].startswith('fe80'):
90 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],94 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
91 addr['netmask']))95 addr['netmask']))
92 if cidr in network:96 if cidr in network:
93 return str(cidr.ip)97 return str(cidr.ip)
9498
95 if fallback is not None:99 if fallback is not None:
96 return fallback100 return fallback
97101
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-18 17:34:36 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2016-02-18 14:28:13 +0000
@@ -14,12 +14,18 @@
14# You should have received a copy of the GNU Lesser General Public License14# 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/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import logging
18import re
19import sys
17import six20import six
18from collections import OrderedDict21from collections import OrderedDict
19from charmhelpers.contrib.amulet.deployment import (22from charmhelpers.contrib.amulet.deployment import (
20 AmuletDeployment23 AmuletDeployment
21)24)
2225
26DEBUG = logging.DEBUG
27ERROR = logging.ERROR
28
2329
24class OpenStackAmuletDeployment(AmuletDeployment):30class OpenStackAmuletDeployment(AmuletDeployment):
25 """OpenStack amulet deployment.31 """OpenStack amulet deployment.
@@ -28,9 +34,12 @@
28 that is specifically for use by OpenStack charms.34 that is specifically for use by OpenStack charms.
29 """35 """
3036
31 def __init__(self, series=None, openstack=None, source=None, stable=True):37 def __init__(self, series=None, openstack=None, source=None,
38 stable=True, log_level=DEBUG):
32 """Initialize the deployment environment."""39 """Initialize the deployment environment."""
33 super(OpenStackAmuletDeployment, self).__init__(series)40 super(OpenStackAmuletDeployment, self).__init__(series)
41 self.log = self.get_logger(level=log_level)
42 self.log.info('OpenStackAmuletDeployment: init')
34 self.openstack = openstack43 self.openstack = openstack
35 self.source = source44 self.source = source
36 self.stable = stable45 self.stable = stable
@@ -38,26 +47,55 @@
38 # out.47 # out.
39 self.current_next = "trusty"48 self.current_next = "trusty"
4049
50 def get_logger(self, name="deployment-logger", level=logging.DEBUG):
51 """Get a logger object that will log to stdout."""
52 log = logging
53 logger = log.getLogger(name)
54 fmt = log.Formatter("%(asctime)s %(funcName)s "
55 "%(levelname)s: %(message)s")
56
57 handler = log.StreamHandler(stream=sys.stdout)
58 handler.setLevel(level)
59 handler.setFormatter(fmt)
60
61 logger.addHandler(handler)
62 logger.setLevel(level)
63
64 return logger
65
41 def _determine_branch_locations(self, other_services):66 def _determine_branch_locations(self, other_services):
42 """Determine the branch locations for the other services.67 """Determine the branch locations for the other services.
4368
44 Determine if the local branch being tested is derived from its69 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding70 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""71 stable or next branches for the other_services."""
72
73 self.log.info('OpenStackAmuletDeployment: determine branch locations')
74
75 # Charms outside the lp:~openstack-charmers namespace
47 base_charms = ['mysql', 'mongodb', 'nrpe']76 base_charms = ['mysql', 'mongodb', 'nrpe']
4877
78 # Force these charms to current series even when using an older series.
79 # ie. Use trusty/nrpe even when series is precise, as the P charm
80 # does not possess the necessary external master config and hooks.
81 force_series_current = ['nrpe']
82
49 if self.series in ['precise', 'trusty']:83 if self.series in ['precise', 'trusty']:
50 base_series = self.series84 base_series = self.series
51 else:85 else:
52 base_series = self.current_next86 base_series = self.current_next
5387
54 if self.stable:88 for svc in other_services:
55 for svc in other_services:89 if svc['name'] in force_series_current:
90 base_series = self.current_next
91 # If a location has been explicitly set, use it
92 if svc.get('location'):
93 continue
94 if self.stable:
56 temp = 'lp:charms/{}/{}'95 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,96 svc['location'] = temp.format(base_series,
58 svc['name'])97 svc['name'])
59 else:98 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:99 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'100 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,101 svc['location'] = temp.format(base_series,
@@ -66,10 +104,13 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'104 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,105 svc['location'] = temp.format(self.current_next,
68 svc['name'])106 svc['name'])
107
69 return other_services108 return other_services
70109
71 def _add_services(self, this_service, other_services):110 def _add_services(self, this_service, other_services):
72 """Add services to the deployment and set openstack-origin/source."""111 """Add services to the deployment and set openstack-origin/source."""
112 self.log.info('OpenStackAmuletDeployment: adding services')
113
73 other_services = self._determine_branch_locations(other_services)114 other_services = self._determine_branch_locations(other_services)
74115
75 super(OpenStackAmuletDeployment, self)._add_services(this_service,116 super(OpenStackAmuletDeployment, self)._add_services(this_service,
@@ -77,29 +118,102 @@
77118
78 services = other_services119 services = other_services
79 services.append(this_service)120 services.append(this_service)
121
122 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',123 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']124 'ceph-osd', 'ceph-radosgw']
82 # Most OpenStack subordinate charms do not expose an origin option125
83 # as that is controlled by the principle.126 # Charms which can not use openstack-origin, ie. many subordinates
84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']127 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
128 'openvswitch-odl', 'neutron-api-odl', 'odl-controller']
85129
86 if self.openstack:130 if self.openstack:
87 for svc in services:131 for svc in services:
88 if svc['name'] not in use_source + ignore:132 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}133 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)134 self.d.configure(svc['name'], config)
91135
92 if self.source:136 if self.source:
93 for svc in services:137 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:138 if svc['name'] in use_source and svc['name'] not in no_origin:
95 config = {'source': self.source}139 config = {'source': self.source}
96 self.d.configure(svc['name'], config)140 self.d.configure(svc['name'], config)
97141
98 def _configure_services(self, configs):142 def _configure_services(self, configs):
99 """Configure all of the services."""143 """Configure all of the services."""
144 self.log.info('OpenStackAmuletDeployment: configure services')
100 for service, config in six.iteritems(configs):145 for service, config in six.iteritems(configs):
101 self.d.configure(service, config)146 self.d.configure(service, config)
102147
148 def _auto_wait_for_status(self, message=None, exclude_services=None,
149 include_only=None, timeout=1800):
150 """Wait for all units to have a specific extended status, except
151 for any defined as excluded. Unless specified via message, any
152 status containing any case of 'ready' will be considered a match.
153
154 Examples of message usage:
155
156 Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
157 message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
158
159 Wait for all units to reach this status (exact match):
160 message = re.compile('^Unit is ready and clustered$')
161
162 Wait for all units to reach any one of these (exact match):
163 message = re.compile('Unit is ready|OK|Ready')
164
165 Wait for at least one unit to reach this status (exact match):
166 message = {'ready'}
167
168 See Amulet's sentry.wait_for_messages() for message usage detail.
169 https://github.com/juju/amulet/blob/master/amulet/sentry.py
170
171 :param message: Expected status match
172 :param exclude_services: List of juju service names to ignore,
173 not to be used in conjuction with include_only.
174 :param include_only: List of juju service names to exclusively check,
175 not to be used in conjuction with exclude_services.
176 :param timeout: Maximum time in seconds to wait for status match
177 :returns: None. Raises if timeout is hit.
178 """
179 self.log.info('Waiting for extended status on units...')
180
181 all_services = self.d.services.keys()
182
183 if exclude_services and include_only:
184 raise ValueError('exclude_services can not be used '
185 'with include_only')
186
187 if message:
188 if isinstance(message, re._pattern_type):
189 match = message.pattern
190 else:
191 match = message
192
193 self.log.debug('Custom extended status wait match: '
194 '{}'.format(match))
195 else:
196 self.log.debug('Default extended status wait match: contains '
197 'READY (case-insensitive)')
198 message = re.compile('.*ready.*', re.IGNORECASE)
199
200 if exclude_services:
201 self.log.debug('Excluding services from extended status match: '
202 '{}'.format(exclude_services))
203 else:
204 exclude_services = []
205
206 if include_only:
207 services = include_only
208 else:
209 services = list(set(all_services) - set(exclude_services))
210
211 self.log.debug('Waiting up to {}s for extended status on services: '
212 '{}'.format(timeout, services))
213 service_messages = {service: message for service in services}
214 self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
215 self.log.info('OK')
216
103 def _get_openstack_release(self):217 def _get_openstack_release(self):
104 """Get openstack release.218 """Get openstack release.
105219
@@ -111,7 +225,8 @@
111 self.precise_havana, self.precise_icehouse,225 self.precise_havana, self.precise_icehouse,
112 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,226 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
113 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,227 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
114 self.wily_liberty) = range(12)228 self.wily_liberty, self.trusty_mitaka,
229 self.xenial_mitaka) = range(14)
115230
116 releases = {231 releases = {
117 ('precise', None): self.precise_essex,232 ('precise', None): self.precise_essex,
@@ -123,9 +238,11 @@
123 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,238 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
124 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,239 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
125 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,240 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
241 ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
126 ('utopic', None): self.utopic_juno,242 ('utopic', None): self.utopic_juno,
127 ('vivid', None): self.vivid_kilo,243 ('vivid', None): self.vivid_kilo,
128 ('wily', None): self.wily_liberty}244 ('wily', None): self.wily_liberty,
245 ('xenial', None): self.xenial_mitaka}
129 return releases[(self.series, self.openstack)]246 return releases[(self.series, self.openstack)]
130247
131 def _get_openstack_release_string(self):248 def _get_openstack_release_string(self):
@@ -142,6 +259,7 @@
142 ('utopic', 'juno'),259 ('utopic', 'juno'),
143 ('vivid', 'kilo'),260 ('vivid', 'kilo'),
144 ('wily', 'liberty'),261 ('wily', 'liberty'),
262 ('xenial', 'mitaka'),
145 ])263 ])
146 if self.openstack:264 if self.openstack:
147 os_origin = self.openstack.split(':')[1]265 os_origin = self.openstack.split(':')[1]
148266
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-17 13:24:05 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2016-02-18 14:28:13 +0000
@@ -18,6 +18,7 @@
18import json18import json
19import logging19import logging
20import os20import os
21import re
21import six22import six
22import time23import time
23import urllib24import urllib
@@ -27,6 +28,7 @@
27import heatclient.v1.client as heat_client28import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client29import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client30import novaclient.v1_1.client as nova_client
31import pika
30import swiftclient32import swiftclient
3133
32from charmhelpers.contrib.amulet.utils import (34from charmhelpers.contrib.amulet.utils import (
@@ -602,3 +604,382 @@
602 self.log.debug('Ceph {} samples (OK): '604 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))605 '{}'.format(sample_type, samples))
604 return None606 return None
607
608 # rabbitmq/amqp specific helpers:
609
610 def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
611 """Wait for rmq units extended status to show cluster readiness,
612 after an optional initial sleep period. Initial sleep is likely
613 necessary to be effective following a config change, as status
614 message may not instantly update to non-ready."""
615
616 if init_sleep:
617 time.sleep(init_sleep)
618
619 message = re.compile('^Unit is ready and clustered$')
620 deployment._auto_wait_for_status(message=message,
621 timeout=timeout,
622 include_only=['rabbitmq-server'])
623
624 def add_rmq_test_user(self, sentry_units,
625 username="testuser1", password="changeme"):
626 """Add a test user via the first rmq juju unit, check connection as
627 the new user against all sentry units.
628
629 :param sentry_units: list of sentry unit pointers
630 :param username: amqp user name, default to testuser1
631 :param password: amqp user password
632 :returns: None if successful. Raise on error.
633 """
634 self.log.debug('Adding rmq user ({})...'.format(username))
635
636 # Check that user does not already exist
637 cmd_user_list = 'rabbitmqctl list_users'
638 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
639 if username in output:
640 self.log.warning('User ({}) already exists, returning '
641 'gracefully.'.format(username))
642 return
643
644 perms = '".*" ".*" ".*"'
645 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
646 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
647
648 # Add user via first unit
649 for cmd in cmds:
650 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
651
652 # Check connection against the other sentry_units
653 self.log.debug('Checking user connect against units...')
654 for sentry_unit in sentry_units:
655 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
656 username=username,
657 password=password)
658 connection.close()
659
660 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
661 """Delete a rabbitmq user via the first rmq juju unit.
662
663 :param sentry_units: list of sentry unit pointers
664 :param username: amqp user name, default to testuser1
665 :param password: amqp user password
666 :returns: None if successful or no such user.
667 """
668 self.log.debug('Deleting rmq user ({})...'.format(username))
669
670 # Check that the user exists
671 cmd_user_list = 'rabbitmqctl list_users'
672 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
673
674 if username not in output:
675 self.log.warning('User ({}) does not exist, returning '
676 'gracefully.'.format(username))
677 return
678
679 # Delete the user
680 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
681 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
682
683 def get_rmq_cluster_status(self, sentry_unit):
684 """Execute rabbitmq cluster status command on a unit and return
685 the full output.
686
687 :param unit: sentry unit
688 :returns: String containing console output of cluster status command
689 """
690 cmd = 'rabbitmqctl cluster_status'
691 output, _ = self.run_cmd_unit(sentry_unit, cmd)
692 self.log.debug('{} cluster_status:\n{}'.format(
693 sentry_unit.info['unit_name'], output))
694 return str(output)
695
696 def get_rmq_cluster_running_nodes(self, sentry_unit):
697 """Parse rabbitmqctl cluster_status output string, return list of
698 running rabbitmq cluster nodes.
699
700 :param unit: sentry unit
701 :returns: List containing node names of running nodes
702 """
703 # NOTE(beisner): rabbitmqctl cluster_status output is not
704 # json-parsable, do string chop foo, then json.loads that.
705 str_stat = self.get_rmq_cluster_status(sentry_unit)
706 if 'running_nodes' in str_stat:
707 pos_start = str_stat.find("{running_nodes,") + 15
708 pos_end = str_stat.find("]},", pos_start) + 1
709 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
710 run_nodes = json.loads(str_run_nodes)
711 return run_nodes
712 else:
713 return []
714
715 def validate_rmq_cluster_running_nodes(self, sentry_units):
716 """Check that all rmq unit hostnames are represented in the
717 cluster_status output of all units.
718
719 :param host_names: dict of juju unit names to host names
720 :param units: list of sentry unit pointers (all rmq units)
721 :returns: None if successful, otherwise return error message
722 """
723 host_names = self.get_unit_hostnames(sentry_units)
724 errors = []
725
726 # Query every unit for cluster_status running nodes
727 for query_unit in sentry_units:
728 query_unit_name = query_unit.info['unit_name']
729 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
730
731 # Confirm that every unit is represented in the queried unit's
732 # cluster_status running nodes output.
733 for validate_unit in sentry_units:
734 val_host_name = host_names[validate_unit.info['unit_name']]
735 val_node_name = 'rabbit@{}'.format(val_host_name)
736
737 if val_node_name not in running_nodes:
738 errors.append('Cluster member check failed on {}: {} not '
739 'in {}\n'.format(query_unit_name,
740 val_node_name,
741 running_nodes))
742 if errors:
743 return ''.join(errors)
744
745 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
746 """Check a single juju rmq unit for ssl and port in the config file."""
747 host = sentry_unit.info['public-address']
748 unit_name = sentry_unit.info['unit_name']
749
750 conf_file = '/etc/rabbitmq/rabbitmq.config'
751 conf_contents = str(self.file_contents_safe(sentry_unit,
752 conf_file, max_wait=16))
753 # Checks
754 conf_ssl = 'ssl' in conf_contents
755 conf_port = str(port) in conf_contents
756
757 # Port explicitly checked in config
758 if port and conf_port and conf_ssl:
759 self.log.debug('SSL is enabled @{}:{} '
760 '({})'.format(host, port, unit_name))
761 return True
762 elif port and not conf_port and conf_ssl:
763 self.log.debug('SSL is enabled @{} but not on port {} '
764 '({})'.format(host, port, unit_name))
765 return False
766 # Port not checked (useful when checking that ssl is disabled)
767 elif not port and conf_ssl:
768 self.log.debug('SSL is enabled @{}:{} '
769 '({})'.format(host, port, unit_name))
770 return True
771 elif not conf_ssl:
772 self.log.debug('SSL not enabled @{}:{} '
773 '({})'.format(host, port, unit_name))
774 return False
775 else:
776 msg = ('Unknown condition when checking SSL status @{}:{} '
777 '({})'.format(host, port, unit_name))
778 amulet.raise_status(amulet.FAIL, msg)
779
780 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
781 """Check that ssl is enabled on rmq juju sentry units.
782
783 :param sentry_units: list of all rmq sentry units
784 :param port: optional ssl port override to validate
785 :returns: None if successful, otherwise return error message
786 """
787 for sentry_unit in sentry_units:
788 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
789 return ('Unexpected condition: ssl is disabled on unit '
790 '({})'.format(sentry_unit.info['unit_name']))
791 return None
792
793 def validate_rmq_ssl_disabled_units(self, sentry_units):
794 """Check that ssl is enabled on listed rmq juju sentry units.
795
796 :param sentry_units: list of all rmq sentry units
797 :returns: True if successful. Raise on error.
798 """
799 for sentry_unit in sentry_units:
800 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
801 return ('Unexpected condition: ssl is enabled on unit '
802 '({})'.format(sentry_unit.info['unit_name']))
803 return None
804
805 def configure_rmq_ssl_on(self, sentry_units, deployment,
806 port=None, max_wait=60):
807 """Turn ssl charm config option on, with optional non-default
808 ssl port specification. Confirm that it is enabled on every
809 unit.
810
811 :param sentry_units: list of sentry units
812 :param deployment: amulet deployment object pointer
813 :param port: amqp port, use defaults if None
814 :param max_wait: maximum time to wait in seconds to confirm
815 :returns: None if successful. Raise on error.
816 """
817 self.log.debug('Setting ssl charm config option: on')
818
819 # Enable RMQ SSL
820 config = {'ssl': 'on'}
821 if port:
822 config['ssl_port'] = port
823
824 deployment.d.configure('rabbitmq-server', config)
825
826 # Wait for unit status
827 self.rmq_wait_for_cluster(deployment)
828
829 # Confirm
830 tries = 0
831 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
832 while ret and tries < (max_wait / 4):
833 time.sleep(4)
834 self.log.debug('Attempt {}: {}'.format(tries, ret))
835 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
836 tries += 1
837
838 if ret:
839 amulet.raise_status(amulet.FAIL, ret)
840
841 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
842 """Turn ssl charm config option off, confirm that it is disabled
843 on every unit.
844
845 :param sentry_units: list of sentry units
846 :param deployment: amulet deployment object pointer
847 :param max_wait: maximum time to wait in seconds to confirm
848 :returns: None if successful. Raise on error.
849 """
850 self.log.debug('Setting ssl charm config option: off')
851
852 # Disable RMQ SSL
853 config = {'ssl': 'off'}
854 deployment.d.configure('rabbitmq-server', config)
855
856 # Wait for unit status
857 self.rmq_wait_for_cluster(deployment)
858
859 # Confirm
860 tries = 0
861 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
862 while ret and tries < (max_wait / 4):
863 time.sleep(4)
864 self.log.debug('Attempt {}: {}'.format(tries, ret))
865 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
866 tries += 1
867
868 if ret:
869 amulet.raise_status(amulet.FAIL, ret)
870
871 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
872 port=None, fatal=True,
873 username="testuser1", password="changeme"):
874 """Establish and return a pika amqp connection to the rabbitmq service
875 running on a rmq juju unit.
876
877 :param sentry_unit: sentry unit pointer
878 :param ssl: boolean, default to False
879 :param port: amqp port, use defaults if None
880 :param fatal: boolean, default to True (raises on connect error)
881 :param username: amqp user name, default to testuser1
882 :param password: amqp user password
883 :returns: pika amqp connection pointer or None if failed and non-fatal
884 """
885 host = sentry_unit.info['public-address']
886 unit_name = sentry_unit.info['unit_name']
887
888 # Default port logic if port is not specified
889 if ssl and not port:
890 port = 5671
891 elif not ssl and not port:
892 port = 5672
893
894 self.log.debug('Connecting to amqp on {}:{} ({}) as '
895 '{}...'.format(host, port, unit_name, username))
896
897 try:
898 credentials = pika.PlainCredentials(username, password)
899 parameters = pika.ConnectionParameters(host=host, port=port,
900 credentials=credentials,
901 ssl=ssl,
902 connection_attempts=3,
903 retry_delay=5,
904 socket_timeout=1)
905 connection = pika.BlockingConnection(parameters)
906 assert connection.server_properties['product'] == 'RabbitMQ'
907 self.log.debug('Connect OK')
908 return connection
909 except Exception as e:
910 msg = ('amqp connection failed to {}:{} as '
911 '{} ({})'.format(host, port, username, str(e)))
912 if fatal:
913 amulet.raise_status(amulet.FAIL, msg)
914 else:
915 self.log.warn(msg)
916 return None
917
918 def publish_amqp_message_by_unit(self, sentry_unit, message,
919 queue="test", ssl=False,
920 username="testuser1",
921 password="changeme",
922 port=None):
923 """Publish an amqp message to a rmq juju unit.
924
925 :param sentry_unit: sentry unit pointer
926 :param message: amqp message string
927 :param queue: message queue, default to test
928 :param username: amqp user name, default to testuser1
929 :param password: amqp user password
930 :param ssl: boolean, default to False
931 :param port: amqp port, use defaults if None
932 :returns: None. Raises exception if publish failed.
933 """
934 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
935 message))
936 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
937 port=port,
938 username=username,
939 password=password)
940
941 # NOTE(beisner): extra debug here re: pika hang potential:
942 # https://github.com/pika/pika/issues/297
943 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
944 self.log.debug('Defining channel...')
945 channel = connection.channel()
946 self.log.debug('Declaring queue...')
947 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
948 self.log.debug('Publishing message...')
949 channel.basic_publish(exchange='', routing_key=queue, body=message)
950 self.log.debug('Closing channel...')
951 channel.close()
952 self.log.debug('Closing connection...')
953 connection.close()
954
955 def get_amqp_message_by_unit(self, sentry_unit, queue="test",
956 username="testuser1",
957 password="changeme",
958 ssl=False, port=None):
959 """Get an amqp message from a rmq juju unit.
960
961 :param sentry_unit: sentry unit pointer
962 :param queue: message queue, default to test
963 :param username: amqp user name, default to testuser1
964 :param password: amqp user password
965 :param ssl: boolean, default to False
966 :param port: amqp port, use defaults if None
967 :returns: amqp message body as string. Raise if get fails.
968 """
969 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
970 port=port,
971 username=username,
972 password=password)
973 channel = connection.channel()
974 method_frame, _, body = channel.basic_get(queue)
975
976 if method_frame:
977 self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
978 body))
979 channel.basic_ack(method_frame.delivery_tag)
980 channel.close()
981 connection.close()
982 return body
983 else:
984 msg = 'No message retrieved.'
985 amulet.raise_status(amulet.FAIL, msg)
605986
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2015-09-12 10:58:20 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2016-02-18 14:28:13 +0000
@@ -14,6 +14,7 @@
14# You should have received a copy of the GNU Lesser General Public License14# 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/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import glob
17import json18import json
18import os19import os
19import re20import re
@@ -56,6 +57,7 @@
56 get_nic_hwaddr,57 get_nic_hwaddr,
57 mkdir,58 mkdir,
58 write_file,59 write_file,
60 pwgen,
59)61)
60from charmhelpers.contrib.hahelpers.cluster import (62from charmhelpers.contrib.hahelpers.cluster import (
61 determine_apache_port,63 determine_apache_port,
@@ -86,6 +88,8 @@
86 is_bridge_member,88 is_bridge_member,
87)89)
88from charmhelpers.contrib.openstack.utils import get_host_ip90from charmhelpers.contrib.openstack.utils import get_host_ip
91from charmhelpers.core.unitdata import kv
92
89CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'93CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
90ADDRESS_TYPES = ['admin', 'internal', 'public']94ADDRESS_TYPES = ['admin', 'internal', 'public']
9195
@@ -194,10 +198,50 @@
194class OSContextGenerator(object):198class OSContextGenerator(object):
195 """Base class for all context generators."""199 """Base class for all context generators."""
196 interfaces = []200 interfaces = []
201 related = False
202 complete = False
203 missing_data = []
197204
198 def __call__(self):205 def __call__(self):
199 raise NotImplementedError206 raise NotImplementedError
200207
208 def context_complete(self, ctxt):
209 """Check for missing data for the required context data.
210 Set self.missing_data if it exists and return False.
211 Set self.complete if no missing data and return True.
212 """
213 # Fresh start
214 self.complete = False
215 self.missing_data = []
216 for k, v in six.iteritems(ctxt):
217 if v is None or v == '':
218 if k not in self.missing_data:
219 self.missing_data.append(k)
220
221 if self.missing_data:
222 self.complete = False
223 log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
224 else:
225 self.complete = True
226 return self.complete
227
228 def get_related(self):
229 """Check if any of the context interfaces have relation ids.
230 Set self.related and return True if one of the interfaces
231 has relation ids.
232 """
233 # Fresh start
234 self.related = False
235 try:
236 for interface in self.interfaces:
237 if relation_ids(interface):
238 self.related = True
239 return self.related
240 except AttributeError as e:
241 log("{} {}"
242 "".format(self, e), 'INFO')
243 return self.related
244
201245
202class SharedDBContext(OSContextGenerator):246class SharedDBContext(OSContextGenerator):
203 interfaces = ['shared-db']247 interfaces = ['shared-db']
@@ -213,6 +257,7 @@
213 self.database = database257 self.database = database
214 self.user = user258 self.user = user
215 self.ssl_dir = ssl_dir259 self.ssl_dir = ssl_dir
260 self.rel_name = self.interfaces[0]
216261
217 def __call__(self):262 def __call__(self):
218 self.database = self.database or config('database')263 self.database = self.database or config('database')
@@ -246,6 +291,7 @@
246 password_setting = self.relation_prefix + '_password'291 password_setting = self.relation_prefix + '_password'
247292
248 for rid in relation_ids(self.interfaces[0]):293 for rid in relation_ids(self.interfaces[0]):
294 self.related = True
249 for unit in related_units(rid):295 for unit in related_units(rid):
250 rdata = relation_get(rid=rid, unit=unit)296 rdata = relation_get(rid=rid, unit=unit)
251 host = rdata.get('db_host')297 host = rdata.get('db_host')
@@ -257,7 +303,7 @@
257 'database_password': rdata.get(password_setting),303 'database_password': rdata.get(password_setting),
258 'database_type': 'mysql'304 'database_type': 'mysql'
259 }305 }
260 if context_complete(ctxt):306 if self.context_complete(ctxt):
261 db_ssl(rdata, ctxt, self.ssl_dir)307 db_ssl(rdata, ctxt, self.ssl_dir)
262 return ctxt308 return ctxt
263 return {}309 return {}
@@ -278,6 +324,7 @@
278324
279 ctxt = {}325 ctxt = {}
280 for rid in relation_ids(self.interfaces[0]):326 for rid in relation_ids(self.interfaces[0]):
327 self.related = True
281 for unit in related_units(rid):328 for unit in related_units(rid):
282 rel_host = relation_get('host', rid=rid, unit=unit)329 rel_host = relation_get('host', rid=rid, unit=unit)
283 rel_user = relation_get('user', rid=rid, unit=unit)330 rel_user = relation_get('user', rid=rid, unit=unit)
@@ -287,7 +334,7 @@
287 'database_user': rel_user,334 'database_user': rel_user,
288 'database_password': rel_passwd,335 'database_password': rel_passwd,
289 'database_type': 'postgresql'}336 'database_type': 'postgresql'}
290 if context_complete(ctxt):337 if self.context_complete(ctxt):
291 return ctxt338 return ctxt
292339
293 return {}340 return {}
@@ -348,6 +395,7 @@
348 ctxt['signing_dir'] = cachedir395 ctxt['signing_dir'] = cachedir
349396
350 for rid in relation_ids(self.rel_name):397 for rid in relation_ids(self.rel_name):
398 self.related = True
351 for unit in related_units(rid):399 for unit in related_units(rid):
352 rdata = relation_get(rid=rid, unit=unit)400 rdata = relation_get(rid=rid, unit=unit)
353 serv_host = rdata.get('service_host')401 serv_host = rdata.get('service_host')
@@ -366,7 +414,7 @@
366 'service_protocol': svc_protocol,414 'service_protocol': svc_protocol,
367 'auth_protocol': auth_protocol})415 'auth_protocol': auth_protocol})
368416
369 if context_complete(ctxt):417 if self.context_complete(ctxt):
370 # NOTE(jamespage) this is required for >= icehouse418 # NOTE(jamespage) this is required for >= icehouse
371 # so a missing value just indicates keystone needs419 # so a missing value just indicates keystone needs
372 # upgrading420 # upgrading
@@ -405,6 +453,7 @@
405 ctxt = {}453 ctxt = {}
406 for rid in relation_ids(self.rel_name):454 for rid in relation_ids(self.rel_name):
407 ha_vip_only = False455 ha_vip_only = False
456 self.related = True
408 for unit in related_units(rid):457 for unit in related_units(rid):
409 if relation_get('clustered', rid=rid, unit=unit):458 if relation_get('clustered', rid=rid, unit=unit):
410 ctxt['clustered'] = True459 ctxt['clustered'] = True
@@ -437,7 +486,7 @@
437 ha_vip_only = relation_get('ha-vip-only',486 ha_vip_only = relation_get('ha-vip-only',
438 rid=rid, unit=unit) is not None487 rid=rid, unit=unit) is not None
439488
440 if context_complete(ctxt):489 if self.context_complete(ctxt):
441 if 'rabbit_ssl_ca' in ctxt:490 if 'rabbit_ssl_ca' in ctxt:
442 if not self.ssl_dir:491 if not self.ssl_dir:
443 log("Charm not setup for ssl support but ssl ca "492 log("Charm not setup for ssl support but ssl ca "
@@ -469,7 +518,7 @@
469 ctxt['oslo_messaging_flags'] = config_flags_parser(518 ctxt['oslo_messaging_flags'] = config_flags_parser(
470 oslo_messaging_flags)519 oslo_messaging_flags)
471520
472 if not context_complete(ctxt):521 if not self.complete:
473 return {}522 return {}
474523
475 return ctxt524 return ctxt
@@ -485,13 +534,15 @@
485534
486 log('Generating template context for ceph', level=DEBUG)535 log('Generating template context for ceph', level=DEBUG)
487 mon_hosts = []536 mon_hosts = []
488 auth = None537 ctxt = {
489 key = None538 'use_syslog': str(config('use-syslog')).lower()
490 use_syslog = str(config('use-syslog')).lower()539 }
491 for rid in relation_ids('ceph'):540 for rid in relation_ids('ceph'):
492 for unit in related_units(rid):541 for unit in related_units(rid):
493 auth = relation_get('auth', rid=rid, unit=unit)542 if not ctxt.get('auth'):
494 key = relation_get('key', rid=rid, unit=unit)543 ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
544 if not ctxt.get('key'):
545 ctxt['key'] = relation_get('key', rid=rid, unit=unit)
495 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,546 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
496 unit=unit)547 unit=unit)
497 unit_priv_addr = relation_get('private-address', rid=rid,548 unit_priv_addr = relation_get('private-address', rid=rid,
@@ -500,15 +551,12 @@
500 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr551 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
501 mon_hosts.append(ceph_addr)552 mon_hosts.append(ceph_addr)
502553
503 ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),554 ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
504 'auth': auth,
505 'key': key,
506 'use_syslog': use_syslog}
507555
508 if not os.path.isdir('/etc/ceph'):556 if not os.path.isdir('/etc/ceph'):
509 os.mkdir('/etc/ceph')557 os.mkdir('/etc/ceph')
510558
511 if not context_complete(ctxt):559 if not self.context_complete(ctxt):
512 return {}560 return {}
513561
514 ensure_packages(['ceph-common'])562 ensure_packages(['ceph-common'])
@@ -581,15 +629,28 @@
581 if config('haproxy-client-timeout'):629 if config('haproxy-client-timeout'):
582 ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')630 ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
583631
632 if config('haproxy-queue-timeout'):
633 ctxt['haproxy_queue_timeout'] = config('haproxy-queue-timeout')
634
635 if config('haproxy-connect-timeout'):
636 ctxt['haproxy_connect_timeout'] = config('haproxy-connect-timeout')
637
584 if config('prefer-ipv6'):638 if config('prefer-ipv6'):
585 ctxt['ipv6'] = True639 ctxt['ipv6'] = True
586 ctxt['local_host'] = 'ip6-localhost'640 ctxt['local_host'] = 'ip6-localhost'
587 ctxt['haproxy_host'] = '::'641 ctxt['haproxy_host'] = '::'
588 ctxt['stat_port'] = ':::8888'
589 else:642 else:
590 ctxt['local_host'] = '127.0.0.1'643 ctxt['local_host'] = '127.0.0.1'
591 ctxt['haproxy_host'] = '0.0.0.0'644 ctxt['haproxy_host'] = '0.0.0.0'
592 ctxt['stat_port'] = ':8888'645
646 ctxt['stat_port'] = '8888'
647
648 db = kv()
649 ctxt['stat_password'] = db.get('stat-password')
650 if not ctxt['stat_password']:
651 ctxt['stat_password'] = db.set('stat-password',
652 pwgen(32))
653 db.flush()
593654
594 for frontend in cluster_hosts:655 for frontend in cluster_hosts:
595 if (len(cluster_hosts[frontend]['backends']) > 1 or656 if (len(cluster_hosts[frontend]['backends']) > 1 or
@@ -907,6 +968,19 @@
907 'config': config}968 'config': config}
908 return ovs_ctxt969 return ovs_ctxt
909970
971 def midonet_ctxt(self):
972 driver = neutron_plugin_attribute(self.plugin, 'driver',
973 self.network_manager)
974 midonet_config = neutron_plugin_attribute(self.plugin, 'config',
975 self.network_manager)
976 mido_ctxt = {'core_plugin': driver,
977 'neutron_plugin': 'midonet',
978 'neutron_security_groups': self.neutron_security_groups,
979 'local_ip': unit_private_ip(),
980 'config': midonet_config}
981
982 return mido_ctxt
983
910 def __call__(self):984 def __call__(self):
911 if self.network_manager not in ['quantum', 'neutron']:985 if self.network_manager not in ['quantum', 'neutron']:
912 return {}986 return {}
@@ -928,6 +1002,8 @@
928 ctxt.update(self.nuage_ctxt())1002 ctxt.update(self.nuage_ctxt())
929 elif self.plugin == 'plumgrid':1003 elif self.plugin == 'plumgrid':
930 ctxt.update(self.pg_ctxt())1004 ctxt.update(self.pg_ctxt())
1005 elif self.plugin == 'midonet':
1006 ctxt.update(self.midonet_ctxt())
9311007
932 alchemy_flags = config('neutron-alchemy-flags')1008 alchemy_flags = config('neutron-alchemy-flags')
933 if alchemy_flags:1009 if alchemy_flags:
@@ -1028,6 +1104,20 @@
1028 config_flags_parser(config_flags)}1104 config_flags_parser(config_flags)}
10291105
10301106
1107class LibvirtConfigFlagsContext(OSContextGenerator):
1108 """
1109 This context provides support for extending
1110 the libvirt section through user-defined flags.
1111 """
1112 def __call__(self):
1113 ctxt = {}
1114 libvirt_flags = config('libvirt-flags')
1115 if libvirt_flags:
1116 ctxt['libvirt_flags'] = config_flags_parser(
1117 libvirt_flags)
1118 return ctxt
1119
1120
1031class SubordinateConfigContext(OSContextGenerator):1121class SubordinateConfigContext(OSContextGenerator):
10321122
1033 """1123 """
@@ -1060,7 +1150,7 @@
10601150
1061 ctxt = {1151 ctxt = {
1062 ... other context ...1152 ... other context ...
1063 'subordinate_config': {1153 'subordinate_configuration': {
1064 'DEFAULT': {1154 'DEFAULT': {
1065 'key1': 'value1',1155 'key1': 'value1',
1066 },1156 },
@@ -1101,22 +1191,23 @@
1101 try:1191 try:
1102 sub_config = json.loads(sub_config)1192 sub_config = json.loads(sub_config)
1103 except:1193 except:
1104 log('Could not parse JSON from subordinate_config '1194 log('Could not parse JSON from '
1105 'setting from %s' % rid, level=ERROR)1195 'subordinate_configuration setting from %s'
1196 % rid, level=ERROR)
1106 continue1197 continue
11071198
1108 for service in self.services:1199 for service in self.services:
1109 if service not in sub_config:1200 if service not in sub_config:
1110 log('Found subordinate_config on %s but it contained'1201 log('Found subordinate_configuration on %s but it '
1111 'nothing for %s service' % (rid, service),1202 'contained nothing for %s service'
1112 level=INFO)1203 % (rid, service), level=INFO)
1113 continue1204 continue
11141205
1115 sub_config = sub_config[service]1206 sub_config = sub_config[service]
1116 if self.config_file not in sub_config:1207 if self.config_file not in sub_config:
1117 log('Found subordinate_config on %s but it contained'1208 log('Found subordinate_configuration on %s but it '
1118 'nothing for %s' % (rid, self.config_file),1209 'contained nothing for %s'
1119 level=INFO)1210 % (rid, self.config_file), level=INFO)
1120 continue1211 continue
11211212
1122 sub_config = sub_config[self.config_file]1213 sub_config = sub_config[self.config_file]
@@ -1319,7 +1410,7 @@
1319 normalized.update({port: port for port in resolved1410 normalized.update({port: port for port in resolved
1320 if port in ports})1411 if port in ports})
1321 if resolved:1412 if resolved:
1322 return {bridge: normalized[port] for port, bridge in1413 return {normalized[port]: bridge for port, bridge in
1323 six.iteritems(portmap) if port in normalized.keys()}1414 six.iteritems(portmap) if port in normalized.keys()}
13241415
1325 return None1416 return None
@@ -1330,12 +1421,22 @@
1330 def __call__(self):1421 def __call__(self):
1331 ctxt = {}1422 ctxt = {}
1332 mappings = super(PhyNICMTUContext, self).__call__()1423 mappings = super(PhyNICMTUContext, self).__call__()
1333 if mappings and mappings.values():1424 if mappings and mappings.keys():
1334 ports = mappings.values()1425 ports = sorted(mappings.keys())
1335 napi_settings = NeutronAPIContext()()1426 napi_settings = NeutronAPIContext()()
1336 mtu = napi_settings.get('network_device_mtu')1427 mtu = napi_settings.get('network_device_mtu')
1428 all_ports = set()
1429 # If any of ports is a vlan device, its underlying device must have
1430 # mtu applied first.
1431 for port in ports:
1432 for lport in glob.glob("/sys/class/net/%s/lower_*" % port):
1433 lport = os.path.basename(lport)
1434 all_ports.add(lport.split('_')[1])
1435
1436 all_ports = list(all_ports)
1437 all_ports.extend(ports)
1337 if mtu:1438 if mtu:
1338 ctxt["devs"] = '\\n'.join(ports)1439 ctxt["devs"] = '\\n'.join(all_ports)
1339 ctxt['mtu'] = mtu1440 ctxt['mtu'] = mtu
13401441
1341 return ctxt1442 return ctxt
@@ -1367,6 +1468,6 @@
1367 'auth_protocol':1468 'auth_protocol':
1368 rdata.get('auth_protocol') or 'http',1469 rdata.get('auth_protocol') or 'http',
1369 }1470 }
1370 if context_complete(ctxt):1471 if self.context_complete(ctxt):
1371 return ctxt1472 return ctxt
1372 return {}1473 return {}
13731474
=== modified file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-02-24 05:48:43 +0000
+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2016-02-18 14:28:13 +0000
@@ -9,15 +9,17 @@
9CRITICAL=09CRITICAL=0
10NOTACTIVE=''10NOTACTIVE=''
11LOGFILE=/var/log/nagios/check_haproxy.log11LOGFILE=/var/log/nagios/check_haproxy.log
12AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')12AUTH=$(grep -r "stats auth" /etc/haproxy | awk 'NR=1{print $4}')
1313
14for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});14typeset -i N_INSTANCES=0
15for appserver in $(awk '/^\s+server/{print $2}' /etc/haproxy/haproxy.cfg)
15do16do
16 output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')17 N_INSTANCES=N_INSTANCES+1
18 output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' --regex=",${appserver},.*,UP.*" -e ' 200 OK')
17 if [ $? != 0 ]; then19 if [ $? != 0 ]; then
18 date >> $LOGFILE20 date >> $LOGFILE
19 echo $output >> $LOGFILE21 echo $output >> $LOGFILE
20 /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&122 /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v | grep ",${appserver}," >> $LOGFILE 2>&1
21 CRITICAL=123 CRITICAL=1
22 NOTACTIVE="${NOTACTIVE} $appserver"24 NOTACTIVE="${NOTACTIVE} $appserver"
23 fi25 fi
@@ -28,5 +30,5 @@
28 exit 230 exit 2
29fi31fi
3032
31echo "OK: All haproxy instances looking good"33echo "OK: All haproxy instances ($N_INSTANCES) looking good"
32exit 034exit 0
3335
=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-09-03 09:42:35 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py 2016-02-18 14:28:13 +0000
@@ -204,11 +204,25 @@
204 database=config('database'),204 database=config('database'),
205 ssl_dir=NEUTRON_CONF_DIR)],205 ssl_dir=NEUTRON_CONF_DIR)],
206 'services': [],206 'services': [],
207 'packages': [['plumgrid-lxc'],207 'packages': ['plumgrid-lxc',
208 ['iovisor-dkms']],208 'iovisor-dkms'],
209 'server_packages': ['neutron-server',209 'server_packages': ['neutron-server',
210 'neutron-plugin-plumgrid'],210 'neutron-plugin-plumgrid'],
211 'server_services': ['neutron-server']211 'server_services': ['neutron-server']
212 },
213 'midonet': {
214 'config': '/etc/neutron/plugins/midonet/midonet.ini',
215 'driver': 'midonet.neutron.plugin.MidonetPluginV2',
216 'contexts': [
217 context.SharedDBContext(user=config('neutron-database-user'),
218 database=config('neutron-database'),
219 relation_prefix='neutron',
220 ssl_dir=NEUTRON_CONF_DIR)],
221 'services': [],
222 'packages': [[headers_package()] + determine_dkms_package()],
223 'server_packages': ['neutron-server',
224 'python-neutron-plugin-midonet'],
225 'server_services': ['neutron-server']
212 }226 }
213 }227 }
214 if release >= 'icehouse':228 if release >= 'icehouse':
@@ -310,10 +324,10 @@
310def parse_data_port_mappings(mappings, default_bridge='br-data'):324def parse_data_port_mappings(mappings, default_bridge='br-data'):
311 """Parse data port mappings.325 """Parse data port mappings.
312326
313 Mappings must be a space-delimited list of port:bridge mappings.327 Mappings must be a space-delimited list of bridge:port.
314328
315 Returns dict of the form {port:bridge} where port may be an mac address or329 Returns dict of the form {port:bridge} where ports may be mac addresses or
316 interface name.330 interface names.
317 """331 """
318332
319 # NOTE(dosaboy): we use rvalue for key to allow multiple values to be333 # NOTE(dosaboy): we use rvalue for key to allow multiple values to be
320334
=== modified file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-07-17 13:24:05 +0000
+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2016-02-18 14:28:13 +0000
@@ -13,3 +13,9 @@
13err to syslog = {{ use_syslog }}13err to syslog = {{ use_syslog }}
14clog to syslog = {{ use_syslog }}14clog to syslog = {{ use_syslog }}
1515
16[client]
17{% if rbd_client_cache_settings -%}
18{% for key, value in rbd_client_cache_settings.iteritems() -%}
19{{ key }} = {{ value }}
20{% endfor -%}
21{%- endif %}
16\ No newline at end of file22\ No newline at end of file
1723
=== modified file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2015-02-24 05:48:43 +0000
+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2016-02-18 14:28:13 +0000
@@ -12,27 +12,35 @@
12 option tcplog12 option tcplog
13 option dontlognull13 option dontlognull
14 retries 314 retries 3
15 timeout queue 100015{%- if haproxy_queue_timeout %}
16 timeout connect 100016 timeout queue {{ haproxy_queue_timeout }}
17{% if haproxy_client_timeout -%}17{%- else %}
18 timeout queue 5000
19{%- endif %}
20{%- if haproxy_connect_timeout %}
21 timeout connect {{ haproxy_connect_timeout }}
22{%- else %}
23 timeout connect 5000
24{%- endif %}
25{%- if haproxy_client_timeout %}
18 timeout client {{ haproxy_client_timeout }}26 timeout client {{ haproxy_client_timeout }}
19{% else -%}27{%- else %}
20 timeout client 3000028 timeout client 30000
21{% endif -%}29{%- endif %}
2230{%- if haproxy_server_timeout %}
23{% if haproxy_server_timeout -%}
24 timeout server {{ haproxy_server_timeout }}31 timeout server {{ haproxy_server_timeout }}
25{% else -%}32{%- else %}
26 timeout server 3000033 timeout server 30000
27{% endif -%}34{%- endif %}
2835
29listen stats {{ stat_port }}36listen stats
37 bind {{ local_host }}:{{ stat_port }}
30 mode http38 mode http
31 stats enable39 stats enable
32 stats hide-version40 stats hide-version
33 stats realm Haproxy\ Statistics41 stats realm Haproxy\ Statistics
34 stats uri /42 stats uri /
35 stats auth admin:password43 stats auth admin:{{ stat_password }}
3644
37{% if frontends -%}45{% if frontends -%}
38{% for service, ports in service_ports.items() -%}46{% for service, ports in service_ports.items() -%}
3947
=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
--- hooks/charmhelpers/contrib/openstack/templating.py 2015-08-27 15:02:34 +0000
+++ hooks/charmhelpers/contrib/openstack/templating.py 2016-02-18 14:28:13 +0000
@@ -18,7 +18,7 @@
1818
19import six19import six
2020
21from charmhelpers.fetch import apt_install21from charmhelpers.fetch import apt_install, apt_update
22from charmhelpers.core.hookenv import (22from charmhelpers.core.hookenv import (
23 log,23 log,
24 ERROR,24 ERROR,
@@ -29,6 +29,7 @@
29try:29try:
30 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions30 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
31except ImportError:31except ImportError:
32 apt_update(fatal=True)
32 apt_install('python-jinja2', fatal=True)33 apt_install('python-jinja2', fatal=True)
33 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions34 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
3435
@@ -112,7 +113,7 @@
112113
113 def complete_contexts(self):114 def complete_contexts(self):
114 '''115 '''
115 Return a list of interfaces that have atisfied contexts.116 Return a list of interfaces that have satisfied contexts.
116 '''117 '''
117 if self._complete_contexts:118 if self._complete_contexts:
118 return self._complete_contexts119 return self._complete_contexts
@@ -293,3 +294,30 @@
293 [interfaces.extend(i.complete_contexts())294 [interfaces.extend(i.complete_contexts())
294 for i in six.itervalues(self.templates)]295 for i in six.itervalues(self.templates)]
295 return interfaces296 return interfaces
297
298 def get_incomplete_context_data(self, interfaces):
299 '''
300 Return dictionary of relation status of interfaces and any missing
301 required context data. Example:
302 {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
303 'zeromq-configuration': {'related': False}}
304 '''
305 incomplete_context_data = {}
306
307 for i in six.itervalues(self.templates):
308 for context in i.contexts:
309 for interface in interfaces:
310 related = False
311 if interface in context.interfaces:
312 related = context.get_related()
313 missing_data = context.missing_data
314 if missing_data:
315 incomplete_context_data[interface] = {'missing_data': missing_data}
316 if related:
317 if incomplete_context_data.get(interface):
318 incomplete_context_data[interface].update({'related': True})
319 else:
320 incomplete_context_data[interface] = {'related': True}
321 else:
322 incomplete_context_data[interface] = {'related': False}
323 return incomplete_context_data
296324
=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py 2015-09-14 20:23:58 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2016-02-18 14:28:13 +0000
@@ -26,6 +26,7 @@
2626
27import six27import six
28import traceback28import traceback
29import uuid
29import yaml30import yaml
3031
31from charmhelpers.contrib.network import ip32from charmhelpers.contrib.network import ip
@@ -41,8 +42,11 @@
41 log as juju_log,42 log as juju_log,
42 charm_dir,43 charm_dir,
43 INFO,44 INFO,
45 related_units,
44 relation_ids,46 relation_ids,
45 relation_set47 relation_set,
48 status_set,
49 hook_name
46)50)
4751
48from charmhelpers.contrib.storage.linux.lvm import (52from charmhelpers.contrib.storage.linux.lvm import (
@@ -52,7 +56,8 @@
52)56)
5357
54from charmhelpers.contrib.network.ip import (58from charmhelpers.contrib.network.ip import (
55 get_ipv6_addr59 get_ipv6_addr,
60 is_ipv6,
56)61)
5762
58from charmhelpers.contrib.python.packages import (63from charmhelpers.contrib.python.packages import (
@@ -81,6 +86,7 @@
81 ('utopic', 'juno'),86 ('utopic', 'juno'),
82 ('vivid', 'kilo'),87 ('vivid', 'kilo'),
83 ('wily', 'liberty'),88 ('wily', 'liberty'),
89 ('xenial', 'mitaka'),
84])90])
8591
8692
@@ -94,6 +100,7 @@
94 ('2014.2', 'juno'),100 ('2014.2', 'juno'),
95 ('2015.1', 'kilo'),101 ('2015.1', 'kilo'),
96 ('2015.2', 'liberty'),102 ('2015.2', 'liberty'),
103 ('2016.1', 'mitaka'),
97])104])
98105
99# The ugly duckling106# The ugly duckling
@@ -118,36 +125,46 @@
118 ('2.2.2', 'kilo'),125 ('2.2.2', 'kilo'),
119 ('2.3.0', 'liberty'),126 ('2.3.0', 'liberty'),
120 ('2.4.0', 'liberty'),127 ('2.4.0', 'liberty'),
128 ('2.5.0', 'liberty'),
121])129])
122130
123# >= Liberty version->codename mapping131# >= Liberty version->codename mapping
124PACKAGE_CODENAMES = {132PACKAGE_CODENAMES = {
125 'nova-common': OrderedDict([133 'nova-common': OrderedDict([
126 ('12.0.0', 'liberty'),134 ('12.0', 'liberty'),
135 ('13.0', 'mitaka'),
127 ]),136 ]),
128 'neutron-common': OrderedDict([137 'neutron-common': OrderedDict([
129 ('7.0.0', 'liberty'),138 ('7.0', 'liberty'),
139 ('8.0', 'mitaka'),
130 ]),140 ]),
131 'cinder-common': OrderedDict([141 'cinder-common': OrderedDict([
132 ('7.0.0', 'liberty'),142 ('7.0', 'liberty'),
143 ('8.0', 'mitaka'),
133 ]),144 ]),
134 'keystone': OrderedDict([145 'keystone': OrderedDict([
135 ('8.0.0', 'liberty'),146 ('8.0', 'liberty'),
147 ('9.0', 'mitaka'),
136 ]),148 ]),
137 'horizon-common': OrderedDict([149 'horizon-common': OrderedDict([
138 ('8.0.0', 'liberty'),150 ('8.0', 'liberty'),
151 ('9.0', 'mitaka'),
139 ]),152 ]),
140 'ceilometer-common': OrderedDict([153 'ceilometer-common': OrderedDict([
141 ('5.0.0', 'liberty'),154 ('5.0', 'liberty'),
155 ('6.0', 'mitaka'),
142 ]),156 ]),
143 'heat-common': OrderedDict([157 'heat-common': OrderedDict([
144 ('5.0.0', 'liberty'),158 ('5.0', 'liberty'),
159 ('6.0', 'mitaka'),
145 ]),160 ]),
146 'glance-common': OrderedDict([161 'glance-common': OrderedDict([
147 ('11.0.0', 'liberty'),162 ('11.0', 'liberty'),
163 ('12.0', 'mitaka'),
148 ]),164 ]),
149 'openstack-dashboard': OrderedDict([165 'openstack-dashboard': OrderedDict([
150 ('8.0.0', 'liberty'),166 ('8.0', 'liberty'),
167 ('9.0', 'mitaka'),
151 ]),168 ]),
152}169}
153170
@@ -234,7 +251,14 @@
234 error_out(e)251 error_out(e)
235252
236 vers = apt.upstream_version(pkg.current_ver.ver_str)253 vers = apt.upstream_version(pkg.current_ver.ver_str)
237 match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)254 if 'swift' in pkg.name:
255 # Fully x.y.z match for swift versions
256 match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
257 else:
258 # x.y match only for 20XX.X
259 # and ignore patch level for other packages
260 match = re.match('^(\d+)\.(\d+)', vers)
261
238 if match:262 if match:
239 vers = match.group(0)263 vers = match.group(0)
240264
@@ -246,13 +270,8 @@
246 # < Liberty co-ordinated project versions270 # < Liberty co-ordinated project versions
247 try:271 try:
248 if 'swift' in pkg.name:272 if 'swift' in pkg.name:
249 swift_vers = vers[:5]273 return SWIFT_CODENAMES[vers]
250 if swift_vers not in SWIFT_CODENAMES:
251 # Deal with 1.10.0 upward
252 swift_vers = vers[:6]
253 return SWIFT_CODENAMES[swift_vers]
254 else:274 else:
255 vers = vers[:6]
256 return OPENSTACK_CODENAMES[vers]275 return OPENSTACK_CODENAMES[vers]
257 except KeyError:276 except KeyError:
258 if not fatal:277 if not fatal:
@@ -371,6 +390,9 @@
371 'liberty': 'trusty-updates/liberty',390 'liberty': 'trusty-updates/liberty',
372 'liberty/updates': 'trusty-updates/liberty',391 'liberty/updates': 'trusty-updates/liberty',
373 'liberty/proposed': 'trusty-proposed/liberty',392 'liberty/proposed': 'trusty-proposed/liberty',
393 'mitaka': 'trusty-updates/mitaka',
394 'mitaka/updates': 'trusty-updates/mitaka',
395 'mitaka/proposed': 'trusty-proposed/mitaka',
374 }396 }
375397
376 try:398 try:
@@ -517,6 +539,12 @@
517 relation_prefix=None):539 relation_prefix=None):
518 hosts = get_ipv6_addr(dynamic_only=False)540 hosts = get_ipv6_addr(dynamic_only=False)
519541
542 if config('vip'):
543 vips = config('vip').split()
544 for vip in vips:
545 if vip and is_ipv6(vip):
546 hosts.append(vip)
547
520 kwargs = {'database': database,548 kwargs = {'database': database,
521 'username': database_user,549 'username': database_user,
522 'hostname': json.dumps(hosts)}550 'hostname': json.dumps(hosts)}
@@ -565,7 +593,7 @@
565 return yaml.load(projects_yaml)593 return yaml.load(projects_yaml)
566594
567595
568def git_clone_and_install(projects_yaml, core_project, depth=1):596def git_clone_and_install(projects_yaml, core_project):
569 """597 """
570 Clone/install all specified OpenStack repositories.598 Clone/install all specified OpenStack repositories.
571599
@@ -615,6 +643,9 @@
615 for p in projects['repositories']:643 for p in projects['repositories']:
616 repo = p['repository']644 repo = p['repository']
617 branch = p['branch']645 branch = p['branch']
646 depth = '1'
647 if 'depth' in p.keys():
648 depth = p['depth']
618 if p['name'] == 'requirements':649 if p['name'] == 'requirements':
619 repo_dir = _git_clone_and_install_single(repo, branch, depth,650 repo_dir = _git_clone_and_install_single(repo, branch, depth,
620 parent_dir, http_proxy,651 parent_dir, http_proxy,
@@ -659,19 +690,13 @@
659 """690 """
660 Clone and install a single git repository.691 Clone and install a single git repository.
661 """692 """
662 dest_dir = os.path.join(parent_dir, os.path.basename(repo))
663
664 if not os.path.exists(parent_dir):693 if not os.path.exists(parent_dir):
665 juju_log('Directory already exists at {}. '694 juju_log('Directory already exists at {}. '
666 'No need to create directory.'.format(parent_dir))695 'No need to create directory.'.format(parent_dir))
667 os.mkdir(parent_dir)696 os.mkdir(parent_dir)
668697
669 if not os.path.exists(dest_dir):698 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
670 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))699 repo_dir = install_remote(repo, dest=parent_dir, branch=branch, depth=depth)
671 repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
672 depth=depth)
673 else:
674 repo_dir = dest_dir
675700
676 venv = os.path.join(parent_dir, 'venv')701 venv = os.path.join(parent_dir, 'venv')
677702
@@ -754,6 +779,178 @@
754 return None779 return None
755780
756781
782def os_workload_status(configs, required_interfaces, charm_func=None):
783 """
784 Decorator to set workload status based on complete contexts
785 """
786 def wrap(f):
787 @wraps(f)
788 def wrapped_f(*args, **kwargs):
789 # Run the original function first
790 f(*args, **kwargs)
791 # Set workload status now that contexts have been
792 # acted on
793 set_os_workload_status(configs, required_interfaces, charm_func)
794 return wrapped_f
795 return wrap
796
797
798def set_os_workload_status(configs, required_interfaces, charm_func=None):
799 """
800 Set workload status based on complete contexts.
801 status-set missing or incomplete contexts
802 and juju-log details of missing required data.
803 charm_func is a charm specific function to run checking
804 for charm specific requirements such as a VIP setting.
805 """
806 incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
807 state = 'active'
808 missing_relations = []
809 incomplete_relations = []
810 message = None
811 charm_state = None
812 charm_message = None
813
814 for generic_interface in incomplete_rel_data.keys():
815 related_interface = None
816 missing_data = {}
817 # Related or not?
818 for interface in incomplete_rel_data[generic_interface]:
819 if incomplete_rel_data[generic_interface][interface].get('related'):
820 related_interface = interface
821 missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
822 # No relation ID for the generic_interface
823 if not related_interface:
824 juju_log("{} relation is missing and must be related for "
825 "functionality. ".format(generic_interface), 'WARN')
826 state = 'blocked'
827 if generic_interface not in missing_relations:
828 missing_relations.append(generic_interface)
829 else:
830 # Relation ID exists but no related unit
831 if not missing_data:
832 # Edge case relation ID exists but departing
833 if ('departed' in hook_name() or 'broken' in hook_name()) \
834 and related_interface in hook_name():
835 state = 'blocked'
836 if generic_interface not in missing_relations:
837 missing_relations.append(generic_interface)
838 juju_log("{} relation's interface, {}, "
839 "relationship is departed or broken "
840 "and is required for functionality."
841 "".format(generic_interface, related_interface), "WARN")
842 # Normal case relation ID exists but no related unit
843 # (joining)
844 else:
845 juju_log("{} relations's interface, {}, is related but has "
846 "no units in the relation."
847 "".format(generic_interface, related_interface), "INFO")
848 # Related unit exists and data missing on the relation
849 else:
850 juju_log("{} relation's interface, {}, is related awaiting "
851 "the following data from the relationship: {}. "
852 "".format(generic_interface, related_interface,
853 ", ".join(missing_data)), "INFO")
854 if state != 'blocked':
855 state = 'waiting'
856 if generic_interface not in incomplete_relations \
857 and generic_interface not in missing_relations:
858 incomplete_relations.append(generic_interface)
859
860 if missing_relations:
861 message = "Missing relations: {}".format(", ".join(missing_relations))
862 if incomplete_relations:
863 message += "; incomplete relations: {}" \
864 "".format(", ".join(incomplete_relations))
865 state = 'blocked'
866 elif incomplete_relations:
867 message = "Incomplete relations: {}" \
868 "".format(", ".join(incomplete_relations))
869 state = 'waiting'
870
871 # Run charm specific checks
872 if charm_func:
873 charm_state, charm_message = charm_func(configs)
874 if charm_state != 'active' and charm_state != 'unknown':
875 state = workload_state_compare(state, charm_state)
876 if message:
877 charm_message = charm_message.replace("Incomplete relations: ",
878 "")
879 message = "{}, {}".format(message, charm_message)
880 else:
881 message = charm_message
882
883 # Set to active if all requirements have been met
884 if state == 'active':
885 message = "Unit is ready"
886 juju_log(message, "INFO")
887
888 status_set(state, message)
889
890
891def workload_state_compare(current_workload_state, workload_state):
892 """ Return highest priority of two states"""
893 hierarchy = {'unknown': -1,
894 'active': 0,
895 'maintenance': 1,
896 'waiting': 2,
897 'blocked': 3,
898 }
899
900 if hierarchy.get(workload_state) is None:
901 workload_state = 'unknown'
902 if hierarchy.get(current_workload_state) is None:
903 current_workload_state = 'unknown'
904
905 # Set workload_state based on hierarchy of statuses
906 if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
907 return current_workload_state
908 else:
909 return workload_state
910
911
912def incomplete_relation_data(configs, required_interfaces):
913 """
914 Check complete contexts against required_interfaces
915 Return dictionary of incomplete relation data.
916
917 configs is an OSConfigRenderer object with configs registered
918
919 required_interfaces is a dictionary of required general interfaces
920 with dictionary values of possible specific interfaces.
921 Example:
922 required_interfaces = {'database': ['shared-db', 'pgsql-db']}
923
924 The interface is said to be satisfied if anyone of the interfaces in the
925 list has a complete context.
926
927 Return dictionary of incomplete or missing required contexts with relation
928 status of interfaces and any missing data points. Example:
929 {'message':
930 {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
931 'zeromq-configuration': {'related': False}},
932 'identity':
933 {'identity-service': {'related': False}},
934 'database':
935 {'pgsql-db': {'related': False},
936 'shared-db': {'related': True}}}
937 """
938 complete_ctxts = configs.complete_contexts()
939 incomplete_relations = []
940 for svc_type in required_interfaces.keys():
941 # Avoid duplicates
942 found_ctxt = False
943 for interface in required_interfaces[svc_type]:
944 if interface in complete_ctxts:
945 found_ctxt = True
946 if not found_ctxt:
947 incomplete_relations.append(svc_type)
948 incomplete_context_data = {}
949 for i in incomplete_relations:
950 incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
951 return incomplete_context_data
952
953
757def do_action_openstack_upgrade(package, upgrade_callback, configs):954def do_action_openstack_upgrade(package, upgrade_callback, configs):
758 """Perform action-managed OpenStack upgrade.955 """Perform action-managed OpenStack upgrade.
759956
@@ -796,3 +993,19 @@
796 action_set({'outcome': 'no upgrade available.'})993 action_set({'outcome': 'no upgrade available.'})
797994
798 return ret995 return ret
996
997
998def remote_restart(rel_name, remote_service=None):
999 trigger = {
1000 'restart-trigger': str(uuid.uuid4()),
1001 }
1002 if remote_service:
1003 trigger['remote-service'] = remote_service
1004 for rid in relation_ids(rel_name):
1005 # This subordinate can be related to two seperate services using
1006 # different subordinate relations so only issue the restart if
1007 # the principle is conencted down the relation we think it is
1008 if related_units(relid=rid):
1009 relation_set(relation_id=rid,
1010 relation_settings=trigger,
1011 )
7991012
=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
--- hooks/charmhelpers/contrib/python/packages.py 2015-06-24 19:07:21 +0000
+++ hooks/charmhelpers/contrib/python/packages.py 2016-02-18 14:28:13 +0000
@@ -42,8 +42,12 @@
42 yield "--{0}={1}".format(key, value)42 yield "--{0}={1}".format(key, value)
4343
4444
45def pip_install_requirements(requirements, **options):45def pip_install_requirements(requirements, constraints=None, **options):
46 """Install a requirements file """46 """Install a requirements file.
47
48 :param constraints: Path to pip constraints file.
49 http://pip.readthedocs.org/en/stable/user_guide/#constraints-files
50 """
47 command = ["install"]51 command = ["install"]
4852
49 available_options = ('proxy', 'src', 'log', )53 available_options = ('proxy', 'src', 'log', )
@@ -51,8 +55,13 @@
51 command.append(option)55 command.append(option)
5256
53 command.append("-r {0}".format(requirements))57 command.append("-r {0}".format(requirements))
54 log("Installing from file: {} with options: {}".format(requirements,58 if constraints:
55 command))59 command.append("-c {0}".format(constraints))
60 log("Installing from file: {} with constraints {} "
61 "and options: {}".format(requirements, constraints, command))
62 else:
63 log("Installing from file: {} with options: {}".format(requirements,
64 command))
56 pip_execute(command)65 pip_execute(command)
5766
5867
5968
=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-07-17 13:24:05 +0000
+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2016-02-18 14:28:13 +0000
@@ -23,11 +23,14 @@
23# James Page <james.page@ubuntu.com>23# James Page <james.page@ubuntu.com>
24# Adam Gandelman <adamg@ubuntu.com>24# Adam Gandelman <adamg@ubuntu.com>
25#25#
26import bisect
27import six
2628
27import os29import os
28import shutil30import shutil
29import json31import json
30import time32import time
33import uuid
3134
32from subprocess import (35from subprocess import (
33 check_call,36 check_call,
@@ -35,8 +38,10 @@
35 CalledProcessError,38 CalledProcessError,
36)39)
37from charmhelpers.core.hookenv import (40from charmhelpers.core.hookenv import (
41 local_unit,
38 relation_get,42 relation_get,
39 relation_ids,43 relation_ids,
44 relation_set,
40 related_units,45 related_units,
41 log,46 log,
42 DEBUG,47 DEBUG,
@@ -56,6 +61,8 @@
56 apt_install,61 apt_install,
57)62)
5863
64from charmhelpers.core.kernel import modprobe
65
59KEYRING = '/etc/ceph/ceph.client.{}.keyring'66KEYRING = '/etc/ceph/ceph.client.{}.keyring'
60KEYFILE = '/etc/ceph/ceph.client.{}.key'67KEYFILE = '/etc/ceph/ceph.client.{}.key'
6168
@@ -67,6 +74,394 @@
67err to syslog = {use_syslog}74err to syslog = {use_syslog}
68clog to syslog = {use_syslog}75clog to syslog = {use_syslog}
69"""76"""
77# For 50 < osds < 240,000 OSDs (Roughly 1 Exabyte at 6T OSDs)
78powers_of_two = [8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608]
79
80
81def validator(value, valid_type, valid_range=None):
82 """
83 Used to validate these: http://docs.ceph.com/docs/master/rados/operations/pools/#set-pool-values
84 Example input:
85 validator(value=1,
86 valid_type=int,
87 valid_range=[0, 2])
88 This says I'm testing value=1. It must be an int inclusive in [0,2]
89
90 :param value: The value to validate
91 :param valid_type: The type that value should be.
92 :param valid_range: A range of values that value can assume.
93 :return:
94 """
95 assert isinstance(value, valid_type), "{} is not a {}".format(
96 value,
97 valid_type)
98 if valid_range is not None:
99 assert isinstance(valid_range, list), \
100 "valid_range must be a list, was given {}".format(valid_range)
101 # If we're dealing with strings
102 if valid_type is six.string_types:
103 assert value in valid_range, \
104 "{} is not in the list {}".format(value, valid_range)
105 # Integer, float should have a min and max
106 else:
107 if len(valid_range) != 2:
108 raise ValueError(
109 "Invalid valid_range list of {} for {}. "
110 "List must be [min,max]".format(valid_range, value))
111 assert value >= valid_range[0], \
112 "{} is less than minimum allowed value of {}".format(
113 value, valid_range[0])
114 assert value <= valid_range[1], \
115 "{} is greater than maximum allowed value of {}".format(
116 value, valid_range[1])
117
118
119class PoolCreationError(Exception):
120 """
121 A custom error to inform the caller that a pool creation failed. Provides an error message
122 """
123 def __init__(self, message):
124 super(PoolCreationError, self).__init__(message)
125
126
127class Pool(object):
128 """
129 An object oriented approach to Ceph pool creation. This base class is inherited by ReplicatedPool and ErasurePool.
130 Do not call create() on this base class as it will not do anything. Instantiate a child class and call create().
131 """
132 def __init__(self, service, name):
133 self.service = service
134 self.name = name
135
136 # Create the pool if it doesn't exist already
137 # To be implemented by subclasses
138 def create(self):
139 pass
140
141 def add_cache_tier(self, cache_pool, mode):
142 """
143 Adds a new cache tier to an existing pool.
144 :param cache_pool: six.string_types. The cache tier pool name to add.
145 :param mode: six.string_types. The caching mode to use for this pool. valid range = ["readonly", "writeback"]
146 :return: None
147 """
148 # Check the input types and values
149 validator(value=cache_pool, valid_type=six.string_types)
150 validator(value=mode, valid_type=six.string_types, valid_range=["readonly", "writeback"])
151
152 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'add', self.name, cache_pool])
153 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, mode])
154 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'set-overlay', self.name, cache_pool])
155 check_call(['ceph', '--id', self.service, 'osd', 'pool', 'set', cache_pool, 'hit_set_type', 'bloom'])
156
157 def remove_cache_tier(self, cache_pool):
158 """
159 Removes a cache tier from Ceph. Flushes all dirty objects from writeback pools and waits for that to complete.
160 :param cache_pool: six.string_types. The cache tier pool name to remove.
161 :return: None
162 """
163 # read-only is easy, writeback is much harder
164 mode = get_cache_mode(cache_pool)
165 if mode == 'readonly':
166 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, 'none'])
167 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove', self.name, cache_pool])
168
169 elif mode == 'writeback':
170 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, 'forward'])
171 # Flush the cache and wait for it to return
172 check_call(['ceph', '--id', self.service, '-p', cache_pool, 'cache-flush-evict-all'])
173 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove-overlay', self.name])
174 check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove', self.name, cache_pool])
175
176 def get_pgs(self, pool_size):
177 """
178 :param pool_size: int. pool_size is either the number of replicas for replicated pools or the K+M sum for
179 erasure coded pools
180 :return: int. The number of pgs to use.
181 """
182 validator(value=pool_size, valid_type=int)
183 osds = get_osds(self.service)
184 if not osds:
185 # NOTE(james-page): Default to 200 for older ceph versions
186 # which don't support OSD query from cli
187 return 200
188
189 # Calculate based on Ceph best practices
190 if osds < 5:
191 return 128
192 elif 5 < osds < 10:
193 return 512
194 elif 10 < osds < 50:
195 return 4096
196 else:
197 estimate = (osds * 100) / pool_size
198 # Return the next nearest power of 2
199 index = bisect.bisect_right(powers_of_two, estimate)
200 return powers_of_two[index]
201
202
203class ReplicatedPool(Pool):
204 def __init__(self, service, name, replicas=2):
205 super(ReplicatedPool, self).__init__(service=service, name=name)
206 self.replicas = replicas
207
208 def create(self):
209 if not pool_exists(self.service, self.name):
210 # Create it
211 pgs = self.get_pgs(self.replicas)
212 cmd = ['ceph', '--id', self.service, 'osd', 'pool', 'create', self.name, str(pgs)]
213 try:
214 check_call(cmd)
215 except CalledProcessError:
216 raise
217
218
219# Default jerasure erasure coded pool
220class ErasurePool(Pool):
221 def __init__(self, service, name, erasure_code_profile="default"):
222 super(ErasurePool, self).__init__(service=service, name=name)
223 self.erasure_code_profile = erasure_code_profile
224
225 def create(self):
226 if not pool_exists(self.service, self.name):
227 # Try to find the erasure profile information so we can properly size the pgs
228 erasure_profile = get_erasure_profile(service=self.service, name=self.erasure_code_profile)
229
230 # Check for errors
231 if erasure_profile is None:
232 log(message='Failed to discover erasure_profile named={}'.format(self.erasure_code_profile),
233 level=ERROR)
234 raise PoolCreationError(message='unable to find erasure profile {}'.format(self.erasure_code_profile))
235 if 'k' not in erasure_profile or 'm' not in erasure_profile:
236 # Error
237 log(message='Unable to find k (data chunks) or m (coding chunks) in {}'.format(erasure_profile),
238 level=ERROR)
239 raise PoolCreationError(
240 message='unable to find k (data chunks) or m (coding chunks) in {}'.format(erasure_profile))
241
242 pgs = self.get_pgs(int(erasure_profile['k']) + int(erasure_profile['m']))
243 # Create it
244 cmd = ['ceph', '--id', self.service, 'osd', 'pool', 'create', self.name, str(pgs),
245 'erasure', self.erasure_code_profile]
246 try:
247 check_call(cmd)
248 except CalledProcessError:
249 raise
250
251 """Get an existing erasure code profile if it already exists.
252 Returns json formatted output"""
253
254
255def get_erasure_profile(service, name):
256 """
257 :param service: six.string_types. The Ceph user name to run the command under
258 :param name:
259 :return:
260 """
261 try:
262 out = check_output(['ceph', '--id', service,
263 'osd', 'erasure-code-profile', 'get',
264 name, '--format=json'])
265 return json.loads(out)
266 except (CalledProcessError, OSError, ValueError):
267 return None
268
269
270def pool_set(service, pool_name, key, value):
271 """
272 Sets a value for a RADOS pool in ceph.
273 :param service: six.string_types. The Ceph user name to run the command under
274 :param pool_name: six.string_types
275 :param key: six.string_types
276 :param value:
277 :return: None. Can raise CalledProcessError
278 """
279 cmd = ['ceph', '--id', service, 'osd', 'pool', 'set', pool_name, key, value]
280 try:
281 check_call(cmd)
282 except CalledProcessError:
283 raise
284
285
286def snapshot_pool(service, pool_name, snapshot_name):
287 """
288 Snapshots a RADOS pool in ceph.
289 :param service: six.string_types. The Ceph user name to run the command under
290 :param pool_name: six.string_types
291 :param snapshot_name: six.string_types
292 :return: None. Can raise CalledProcessError
293 """
294 cmd = ['ceph', '--id', service, 'osd', 'pool', 'mksnap', pool_name, snapshot_name]
295 try:
296 check_call(cmd)
297 except CalledProcessError:
298 raise
299
300
301def remove_pool_snapshot(service, pool_name, snapshot_name):
302 """
303 Remove a snapshot from a RADOS pool in ceph.
304 :param service: six.string_types. The Ceph user name to run the command under
305 :param pool_name: six.string_types
306 :param snapshot_name: six.string_types
307 :return: None. Can raise CalledProcessError
308 """
309 cmd = ['ceph', '--id', service, 'osd', 'pool', 'rmsnap', pool_name, snapshot_name]
310 try:
311 check_call(cmd)
312 except CalledProcessError:
313 raise
314
315
316# max_bytes should be an int or long
317def set_pool_quota(service, pool_name, max_bytes):
318 """
319 :param service: six.string_types. The Ceph user name to run the command under
320 :param pool_name: six.string_types
321 :param max_bytes: int or long
322 :return: None. Can raise CalledProcessError
323 """
324 # Set a byte quota on a RADOS pool in ceph.
325 cmd = ['ceph', '--id', service, 'osd', 'pool', 'set-quota', pool_name, 'max_bytes', max_bytes]
326 try:
327 check_call(cmd)
328 except CalledProcessError:
329 raise
330
331
332def remove_pool_quota(service, pool_name):
333 """
334 Set a byte quota on a RADOS pool in ceph.
335 :param service: six.string_types. The Ceph user name to run the command under
336 :param pool_name: six.string_types
337 :return: None. Can raise CalledProcessError
338 """
339 cmd = ['ceph', '--id', service, 'osd', 'pool', 'set-quota', pool_name, 'max_bytes', '0']
340 try:
341 check_call(cmd)
342 except CalledProcessError:
343 raise
344
345
346def create_erasure_profile(service, profile_name, erasure_plugin_name='jerasure', failure_domain='host',
347 data_chunks=2, coding_chunks=1,
348 locality=None, durability_estimator=None):
349 """
350 Create a new erasure code profile if one does not already exist for it. Updates
351 the profile if it exists. Please see http://docs.ceph.com/docs/master/rados/operations/erasure-code-profile/
352 for more details
353 :param service: six.string_types. The Ceph user name to run the command under
354 :param profile_name: six.string_types
355 :param erasure_plugin_name: six.string_types
356 :param failure_domain: six.string_types. One of ['chassis', 'datacenter', 'host', 'osd', 'pdu', 'pod', 'rack', 'region',
357 'room', 'root', 'row'])
358 :param data_chunks: int
359 :param coding_chunks: int
360 :param locality: int
361 :param durability_estimator: int
362 :return: None. Can raise CalledProcessError
363 """
364 # Ensure this failure_domain is allowed by Ceph
365 validator(failure_domain, six.string_types,
366 ['chassis', 'datacenter', 'host', 'osd', 'pdu', 'pod', 'rack', 'region', 'room', 'root', 'row'])
367
368 cmd = ['ceph', '--id', service, 'osd', 'erasure-code-profile', 'set', profile_name,
369 'plugin=' + erasure_plugin_name, 'k=' + str(data_chunks), 'm=' + str(coding_chunks),
370 'ruleset_failure_domain=' + failure_domain]
371 if locality is not None and durability_estimator is not None:
372 raise ValueError("create_erasure_profile should be called with k, m and one of l or c but not both.")
373
374 # Add plugin specific information
375 if locality is not None:
376 # For local erasure codes
377 cmd.append('l=' + str(locality))
378 if durability_estimator is not None:
379 # For Shec erasure codes
380 cmd.append('c=' + str(durability_estimator))
381
382 if erasure_profile_exists(service, profile_name):
383 cmd.append('--force')
384
385 try:
386 check_call(cmd)
387 except CalledProcessError:
388 raise
389
390
391def rename_pool(service, old_name, new_name):
392 """
393 Rename a Ceph pool from old_name to new_name
394 :param service: six.string_types. The Ceph user name to run the command under
395 :param old_name: six.string_types
396 :param new_name: six.string_types
397 :return: None
398 """
399 validator(value=old_name, valid_type=six.string_types)
400 validator(value=new_name, valid_type=six.string_types)
401
402 cmd = ['ceph', '--id', service, 'osd', 'pool', 'rename', old_name, new_name]
403 check_call(cmd)
404
405
406def erasure_profile_exists(service, name):
407 """
408 Check to see if an Erasure code profile already exists.
409 :param service: six.string_types. The Ceph user name to run the command under
410 :param name: six.string_types
411 :return: int or None
412 """
413 validator(value=name, valid_type=six.string_types)
414 try:
415 check_call(['ceph', '--id', service,
416 'osd', 'erasure-code-profile', 'get',
417 name])
418 return True
419 except CalledProcessError:
420 return False
421
422
423def get_cache_mode(service, pool_name):
424 """
425 Find the current caching mode of the pool_name given.
426 :param service: six.string_types. The Ceph user name to run the command under
427 :param pool_name: six.string_types
428 :return: int or None
429 """
430 validator(value=service, valid_type=six.string_types)
431 validator(value=pool_name, valid_type=six.string_types)
432 out = check_output(['ceph', '--id', service, 'osd', 'dump', '--format=json'])
433 try:
434 osd_json = json.loads(out)
435 for pool in osd_json['pools']:
436 if pool['pool_name'] == pool_name:
437 return pool['cache_mode']
438 return None
439 except ValueError:
440 raise
441
442
443def pool_exists(service, name):
444 """Check to see if a RADOS pool already exists."""
445 try:
446 out = check_output(['rados', '--id', service,
447 'lspools']).decode('UTF-8')
448 except CalledProcessError:
449 return False
450
451 return name in out
452
453
454def get_osds(service):
455 """Return a list of all Ceph Object Storage Daemons currently in the
456 cluster.
457 """
458 version = ceph_version()
459 if version and version >= '0.56':
460 return json.loads(check_output(['ceph', '--id', service,
461 'osd', 'ls',
462 '--format=json']).decode('UTF-8'))
463
464 return None
70465
71466
72def install():467def install():
@@ -96,53 +491,37 @@
96 check_call(cmd)491 check_call(cmd)
97492
98493
99def pool_exists(service, name):494def update_pool(client, pool, settings):
100 """Check to see if a RADOS pool already exists."""495 cmd = ['ceph', '--id', client, 'osd', 'pool', 'set', pool]
101 try:496 for k, v in six.iteritems(settings):
102 out = check_output(['rados', '--id', service,497 cmd.append(k)
103 'lspools']).decode('UTF-8')498 cmd.append(v)
104 except CalledProcessError:499
105 return False500 check_call(cmd)
106501
107 return name in out502
108503def create_pool(service, name, replicas=3, pg_num=None):
109
110def get_osds(service):
111 """Return a list of all Ceph Object Storage Daemons currently in the
112 cluster.
113 """
114 version = ceph_version()
115 if version and version >= '0.56':
116 return json.loads(check_output(['ceph', '--id', service,
117 'osd', 'ls',
118 '--format=json']).decode('UTF-8'))
119
120 return None
121
122
123def create_pool(service, name, replicas=3):
124 """Create a new RADOS pool."""504 """Create a new RADOS pool."""
125 if pool_exists(service, name):505 if pool_exists(service, name):
126 log("Ceph pool {} already exists, skipping creation".format(name),506 log("Ceph pool {} already exists, skipping creation".format(name),
127 level=WARNING)507 level=WARNING)
128 return508 return
129509
130 # Calculate the number of placement groups based510 if not pg_num:
131 # on upstream recommended best practices.511 # Calculate the number of placement groups based
132 osds = get_osds(service)512 # on upstream recommended best practices.
133 if osds:513 osds = get_osds(service)
134 pgnum = (len(osds) * 100 // replicas)514 if osds:
135 else:515 pg_num = (len(osds) * 100 // replicas)
136 # NOTE(james-page): Default to 200 for older ceph versions516 else:
137 # which don't support OSD query from cli517 # NOTE(james-page): Default to 200 for older ceph versions
138 pgnum = 200518 # which don't support OSD query from cli
139519 pg_num = 200
140 cmd = ['ceph', '--id', service, 'osd', 'pool', 'create', name, str(pgnum)]520
141 check_call(cmd)521 cmd = ['ceph', '--id', service, 'osd', 'pool', 'create', name, str(pg_num)]
142522 check_call(cmd)
143 cmd = ['ceph', '--id', service, 'osd', 'pool', 'set', name, 'size',523
144 str(replicas)]524 update_pool(service, name, settings={'size': str(replicas)})
145 check_call(cmd)
146525
147526
148def delete_pool(service, name):527def delete_pool(service, name):
@@ -197,10 +576,10 @@
197 log('Created new keyfile at %s.' % keyfile, level=INFO)576 log('Created new keyfile at %s.' % keyfile, level=INFO)
198577
199578
200def get_ceph_nodes():579def get_ceph_nodes(relation='ceph'):
201 """Query named relation 'ceph' to determine current nodes."""580 """Query named relation to determine current nodes."""
202 hosts = []581 hosts = []
203 for r_id in relation_ids('ceph'):582 for r_id in relation_ids(relation):
204 for unit in related_units(r_id):583 for unit in related_units(r_id):
205 hosts.append(relation_get('private-address', unit=unit, rid=r_id))584 hosts.append(relation_get('private-address', unit=unit, rid=r_id))
206585
@@ -288,17 +667,6 @@
288 os.chown(data_src_dst, uid, gid)667 os.chown(data_src_dst, uid, gid)
289668
290669
291# TODO: re-use
292def modprobe(module):
293 """Load a kernel module and configure for auto-load on reboot."""
294 log('Loading kernel module', level=INFO)
295 cmd = ['modprobe', module]
296 check_call(cmd)
297 with open('/etc/modules', 'r+') as modules:
298 if module not in modules.read():
299 modules.write(module)
300
301
302def copy_files(src, dst, symlinks=False, ignore=None):670def copy_files(src, dst, symlinks=False, ignore=None):
303 """Copy files from src to dst."""671 """Copy files from src to dst."""
304 for item in os.listdir(src):672 for item in os.listdir(src):
@@ -363,14 +731,14 @@
363 service_start(svc)731 service_start(svc)
364732
365733
366def ensure_ceph_keyring(service, user=None, group=None):734def ensure_ceph_keyring(service, user=None, group=None, relation='ceph'):
367 """Ensures a ceph keyring is created for a named service and optionally735 """Ensures a ceph keyring is created for a named service and optionally
368 ensures user and group ownership.736 ensures user and group ownership.
369737
370 Returns False if no ceph key is available in relation state.738 Returns False if no ceph key is available in relation state.
371 """739 """
372 key = None740 key = None
373 for rid in relation_ids('ceph'):741 for rid in relation_ids(relation):
374 for unit in related_units(rid):742 for unit in related_units(rid):
375 key = relation_get('key', rid=rid, unit=unit)743 key = relation_get('key', rid=rid, unit=unit)
376 if key:744 if key:
@@ -411,17 +779,60 @@
411779
412 The API is versioned and defaults to version 1.780 The API is versioned and defaults to version 1.
413 """781 """
414 def __init__(self, api_version=1):782
783 def __init__(self, api_version=1, request_id=None):
415 self.api_version = api_version784 self.api_version = api_version
785 if request_id:
786 self.request_id = request_id
787 else:
788 self.request_id = str(uuid.uuid1())
416 self.ops = []789 self.ops = []
417790
418 def add_op_create_pool(self, name, replica_count=3):791 def add_op_create_pool(self, name, replica_count=3, pg_num=None):
792 """Adds an operation to create a pool.
793
794 @param pg_num setting: optional setting. If not provided, this value
795 will be calculated by the broker based on how many OSDs are in the
796 cluster at the time of creation. Note that, if provided, this value
797 will be capped at the current available maximum.
798 """
419 self.ops.append({'op': 'create-pool', 'name': name,799 self.ops.append({'op': 'create-pool', 'name': name,
420 'replicas': replica_count})800 'replicas': replica_count, 'pg_num': pg_num})
801
802 def set_ops(self, ops):
803 """Set request ops to provided value.
804
805 Useful for injecting ops that come from a previous request
806 to allow comparisons to ensure validity.
807 """
808 self.ops = ops
421809
422 @property810 @property
423 def request(self):811 def request(self):
424 return json.dumps({'api-version': self.api_version, 'ops': self.ops})812 return json.dumps({'api-version': self.api_version, 'ops': self.ops,
813 'request-id': self.request_id})
814
815 def _ops_equal(self, other):
816 if len(self.ops) == len(other.ops):
817 for req_no in range(0, len(self.ops)):
818 for key in ['replicas', 'name', 'op', 'pg_num']:
819 if self.ops[req_no].get(key) != other.ops[req_no].get(key):
820 return False
821 else:
822 return False
823 return True
824
825 def __eq__(self, other):
826 if not isinstance(other, self.__class__):
827 return False
828 if self.api_version == other.api_version and \
829 self._ops_equal(other):
830 return True
831 else:
832 return False
833
834 def __ne__(self, other):
835 return not self.__eq__(other)
425836
426837
427class CephBrokerRsp(object):838class CephBrokerRsp(object):
@@ -431,14 +842,198 @@
431842
432 The API is versioned and defaults to version 1.843 The API is versioned and defaults to version 1.
433 """844 """
845
434 def __init__(self, encoded_rsp):846 def __init__(self, encoded_rsp):
435 self.api_version = None847 self.api_version = None
436 self.rsp = json.loads(encoded_rsp)848 self.rsp = json.loads(encoded_rsp)
437849
438 @property850 @property
851 def request_id(self):
852 return self.rsp.get('request-id')
853
854 @property
439 def exit_code(self):855 def exit_code(self):
440 return self.rsp.get('exit-code')856 return self.rsp.get('exit-code')
441857
442 @property858 @property
443 def exit_msg(self):859 def exit_msg(self):
444 return self.rsp.get('stderr')860 return self.rsp.get('stderr')
861
862
863# Ceph Broker Conversation:
864# If a charm needs an action to be taken by ceph it can create a CephBrokerRq
865# and send that request to ceph via the ceph relation. The CephBrokerRq has a
866# unique id so that the client can identity which CephBrokerRsp is associated
867# with the request. Ceph will also respond to each client unit individually
868# creating a response key per client unit eg glance/0 will get a CephBrokerRsp
869# via key broker-rsp-glance-0
870#
871# To use this the charm can just do something like:
872#
873# from charmhelpers.contrib.storage.linux.ceph import (
874# send_request_if_needed,
875# is_request_complete,
876# CephBrokerRq,
877# )
878#
879# @hooks.hook('ceph-relation-changed')
880# def ceph_changed():
881# rq = CephBrokerRq()
882# rq.add_op_create_pool(name='poolname', replica_count=3)
883#
884# if is_request_complete(rq):
885# <Request complete actions>
886# else:
887# send_request_if_needed(get_ceph_request())
888#
889# CephBrokerRq and CephBrokerRsp are serialized into JSON. Below is an example
890# of glance having sent a request to ceph which ceph has successfully processed
891# 'ceph:8': {
892# 'ceph/0': {
893# 'auth': 'cephx',
894# 'broker-rsp-glance-0': '{"request-id": "0bc7dc54", "exit-code": 0}',
895# 'broker_rsp': '{"request-id": "0da543b8", "exit-code": 0}',
896# 'ceph-public-address': '10.5.44.103',
897# 'key': 'AQCLDttVuHXINhAAvI144CB09dYchhHyTUY9BQ==',
898# 'private-address': '10.5.44.103',
899# },
900# 'glance/0': {
901# 'broker_req': ('{"api-version": 1, "request-id": "0bc7dc54", '
902# '"ops": [{"replicas": 3, "name": "glance", '
903# '"op": "create-pool"}]}'),
904# 'private-address': '10.5.44.109',
905# },
906# }
907
908def get_previous_request(rid):
909 """Return the last ceph broker request sent on a given relation
910
911 @param rid: Relation id to query for request
912 """
913 request = None
914 broker_req = relation_get(attribute='broker_req', rid=rid,
915 unit=local_unit())
916 if broker_req:
917 request_data = json.loads(broker_req)
918 request = CephBrokerRq(api_version=request_data['api-version'],
919 request_id=request_data['request-id'])
920 request.set_ops(request_data['ops'])
921
922 return request
923
924
925def get_request_states(request, relation='ceph'):
926 """Return a dict of requests per relation id with their corresponding
927 completion state.
928
929 This allows a charm, which has a request for ceph, to see whether there is
930 an equivalent request already being processed and if so what state that
931 request is in.
932
933 @param request: A CephBrokerRq object
934 """
935 complete = []
936 requests = {}
937 for rid in relation_ids(relation):
938 complete = False
939 previous_request = get_previous_request(rid)
940 if request == previous_request:
941 sent = True
942 complete = is_request_complete_for_rid(previous_request, rid)
943 else:
944 sent = False
945 complete = False
946
947 requests[rid] = {
948 'sent': sent,
949 'complete': complete,
950 }
951
952 return requests
953
954
955def is_request_sent(request, relation='ceph'):
956 """Check to see if a functionally equivalent request has already been sent
957
958 Returns True if a similair request has been sent
959
960 @param request: A CephBrokerRq object
961 """
962 states = get_request_states(request, relation=relation)
963 for rid in states.keys():
964 if not states[rid]['sent']:
965 return False
966
967 return True
968
969
970def is_request_complete(request, relation='ceph'):
971 """Check to see if a functionally equivalent request has already been
972 completed
973
974 Returns True if a similair request has been completed
975
976 @param request: A CephBrokerRq object
977 """
978 states = get_request_states(request, relation=relation)
979 for rid in states.keys():
980 if not states[rid]['complete']:
981 return False
982
983 return True
984
985
986def is_request_complete_for_rid(request, rid):
987 """Check if a given request has been completed on the given relation
988
989 @param request: A CephBrokerRq object
990 @param rid: Relation ID
991 """
992 broker_key = get_broker_rsp_key()
993 for unit in related_units(rid):
994 rdata = relation_get(rid=rid, unit=unit)
995 if rdata.get(broker_key):
996 rsp = CephBrokerRsp(rdata.get(broker_key))
997 if rsp.request_id == request.request_id:
998 if not rsp.exit_code:
999 return True
1000 else:
1001 # The remote unit sent no reply targeted at this unit so either the
1002 # remote ceph cluster does not support unit targeted replies or it
1003 # has not processed our request yet.
1004 if rdata.get('broker_rsp'):
1005 request_data = json.loads(rdata['broker_rsp'])
1006 if request_data.get('request-id'):
1007 log('Ignoring legacy broker_rsp without unit key as remote '
1008 'service supports unit specific replies', level=DEBUG)
1009 else:
1010 log('Using legacy broker_rsp as remote service does not '
1011 'supports unit specific replies', level=DEBUG)
1012 rsp = CephBrokerRsp(rdata['broker_rsp'])
1013 if not rsp.exit_code:
1014 return True
1015
1016 return False
1017
1018
1019def get_broker_rsp_key():
1020 """Return broker response key for this unit
1021
1022 This is the key that ceph is going to use to pass request status
1023 information back to this unit
1024 """
1025 return 'broker-rsp-' + local_unit().replace('/', '-')
1026
1027
1028def send_request_if_needed(request, relation='ceph'):
1029 """Send broker request if an equivalent request has not already been sent
1030
1031 @param request: A CephBrokerRq object
1032 """
1033 if is_request_sent(request, relation=relation):
1034 log('Request already sent but not complete, not sending new request',
1035 level=DEBUG)
1036 else:
1037 for rid in relation_ids(relation):
1038 log('Sending request {}'.format(request.request_id), level=DEBUG)
1039 relation_set(relation_id=rid, broker_req=request.request)
4451040
=== modified file 'hooks/charmhelpers/contrib/storage/linux/loopback.py'
--- hooks/charmhelpers/contrib/storage/linux/loopback.py 2015-01-26 09:46:38 +0000
+++ hooks/charmhelpers/contrib/storage/linux/loopback.py 2016-02-18 14:28:13 +0000
@@ -76,3 +76,13 @@
76 check_call(cmd)76 check_call(cmd)
7777
78 return create_loopback(path)78 return create_loopback(path)
79
80
81def is_mapped_loopback_device(device):
82 """
83 Checks if a given device name is an existing/mapped loopback device.
84 :param device: str: Full path to the device (eg, /dev/loop1).
85 :returns: str: Path to the backing file if is a loopback device
86 empty string otherwise
87 """
88 return loopback_devices().get(device, "")
7989
=== added file 'hooks/charmhelpers/core/files.py'
--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/files.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/core/files.py'
--- hooks/charmhelpers/core/files.py 2015-07-29 10:48:39 +0000
+++ hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
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)])
460
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-09-03 09:42:35 +0000
+++ hooks/charmhelpers/core/hookenv.py 2016-02-18 14:28:13 +0000
@@ -491,6 +491,19 @@
491491
492492
493@cached493@cached
494def peer_relation_id():
495 '''Get the peers relation id if a peers relation has been joined, else None.'''
496 md = metadata()
497 section = md.get('peers')
498 if section:
499 for key in section:
500 relids = relation_ids(key)
501 if relids:
502 return relids[0]
503 return None
504
505
506@cached
494def relation_to_interface(relation_name):507def relation_to_interface(relation_name):
495 """508 """
496 Given the name of a relation, return the interface that relation uses.509 Given the name of a relation, return the interface that relation uses.
@@ -504,12 +517,12 @@
504def relation_to_role_and_interface(relation_name):517def relation_to_role_and_interface(relation_name):
505 """518 """
506 Given the name of a relation, return the role and the name of the interface519 Given the name of a relation, return the role and the name of the interface
507 that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).520 that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
508521
509 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.522 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
510 """523 """
511 _metadata = metadata()524 _metadata = metadata()
512 for role in ('provides', 'requires', 'peer'):525 for role in ('provides', 'requires', 'peers'):
513 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')526 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
514 if interface:527 if interface:
515 return role, interface528 return role, interface
@@ -521,7 +534,7 @@
521 """534 """
522 Given a role and interface name, return a list of relation names for the535 Given a role and interface name, return a list of relation names for the
523 current charm that use that interface under that role (where role is one536 current charm that use that interface under that role (where role is one
524 of ``provides``, ``requires``, or ``peer``).537 of ``provides``, ``requires``, or ``peers``).
525538
526 :returns: A list of relation names.539 :returns: A list of relation names.
527 """540 """
@@ -542,7 +555,7 @@
542 :returns: A list of relation names.555 :returns: A list of relation names.
543 """556 """
544 results = []557 results = []
545 for role in ('provides', 'requires', 'peer'):558 for role in ('provides', 'requires', 'peers'):
546 results.extend(role_and_interface_to_relations(role, interface_name))559 results.extend(role_and_interface_to_relations(role, interface_name))
547 return results560 return results
548561
@@ -623,6 +636,38 @@
623 return unit_get('private-address')636 return unit_get('private-address')
624637
625638
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
626class UnregisteredHookError(Exception):671class UnregisteredHookError(Exception):
627 """Raised when an undefined hook is called"""672 """Raised when an undefined hook is called"""
628 pass673 pass
@@ -788,6 +833,7 @@
788833
789def translate_exc(from_exc, to_exc):834def translate_exc(from_exc, to_exc):
790 def inner_translate_exc1(f):835 def inner_translate_exc1(f):
836 @wraps(f)
791 def inner_translate_exc2(*args, **kwargs):837 def inner_translate_exc2(*args, **kwargs):
792 try:838 try:
793 return f(*args, **kwargs)839 return f(*args, **kwargs)
@@ -832,6 +878,40 @@
832 subprocess.check_call(cmd)878 subprocess.check_call(cmd)
833879
834880
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
835@cached915@cached
836def juju_version():916def juju_version():
837 """Full version string (eg. '1.23.3.1-trusty-amd64')"""917 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
838918
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-08-27 15:02:34 +0000
+++ hooks/charmhelpers/core/host.py 2016-02-18 14:28:13 +0000
@@ -63,55 +63,85 @@
63 return service_result63 return service_result
6464
6565
66def service_pause(service_name, init_dir=None):66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67 """Pause a system service.67 """Pause a system service.
6868
69 Stop it, and prevent it from starting again at boot."""69 Stop it, and prevent it from starting again at boot."""
70 if init_dir is None:70 stopped = True
71 init_dir = "/etc/init"71 if service_running(service_name):
72 stopped = service_stop(service_name)72 stopped = service_stop(service_name)
73 # XXX: Support systemd too73 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 override_path = os.path.join(74 sysv_file = os.path.join(initd_dir, service_name)
75 init_dir, '{}.override'.format(service_name))75 if init_is_systemd():
76 with open(override_path, 'w') as fh:76 service('disable', service_name)
77 fh.write("manual\n")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))
78 return stopped89 return stopped
7990
8091
81def service_resume(service_name, init_dir=None):92def service_resume(service_name, init_dir="/etc/init",
93 initd_dir="/etc/init.d"):
82 """Resume a system service.94 """Resume a system service.
8395
84 Reenable starting again at boot. Start the service"""96 Reenable starting again at boot. Start the service"""
85 # XXX: Support systemd too97 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
86 if init_dir is None:98 sysv_file = os.path.join(initd_dir, service_name)
87 init_dir = "/etc/init"99 if init_is_systemd():
88 override_path = os.path.join(100 service('enable', service_name)
89 init_dir, '{}.override'.format(service_name))101 elif os.path.exists(upstart_file):
90 if os.path.exists(override_path):102 override_path = os.path.join(
91 os.unlink(override_path)103 init_dir, '{}.override'.format(service_name))
92 started = service_start(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)
93 return started117 return started
94118
95119
96def service(action, service_name):120def service(action, service_name):
97 """Control a system service"""121 """Control a system service"""
98 cmd = ['service', service_name, action]122 if init_is_systemd():
123 cmd = ['systemctl', action, service_name]
124 else:
125 cmd = ['service', service_name, action]
99 return subprocess.call(cmd) == 0126 return subprocess.call(cmd) == 0
100127
101128
102def service_running(service):129def service_running(service_name):
103 """Determine whether a system service is running"""130 """Determine whether a system service is running"""
104 try:131 if init_is_systemd():
105 output = subprocess.check_output(132 return service('is-active', service_name)
106 ['service', service, 'status'],
107 stderr=subprocess.STDOUT).decode('UTF-8')
108 except subprocess.CalledProcessError:
109 return False
110 else:133 else:
111 if ("start/running" in output or "is running" in output):134 try:
112 return True135 output = subprocess.check_output(
113 else:136 ['service', service_name, 'status'],
137 stderr=subprocess.STDOUT).decode('UTF-8')
138 except subprocess.CalledProcessError:
114 return False139 return False
140 else:
141 if ("start/running" in output or "is running" in output):
142 return True
143 else:
144 return False
115145
116146
117def service_available(service_name):147def service_available(service_name):
@@ -126,8 +156,29 @@
126 return True156 return True
127157
128158
129def adduser(username, password=None, shell='/bin/bash', system_user=False):159SYSTEMD_SYSTEM = '/run/systemd/system'
130 """Add a user to the system"""160
161
162def init_is_systemd():
163 return os.path.isdir(SYSTEMD_SYSTEM)
164
165
166def adduser(username, password=None, shell='/bin/bash', system_user=False,
167 primary_group=None, secondary_groups=None):
168 """
169 Add a user to the system.
170
171 Will log but otherwise succeed if the user already exists.
172
173 :param str username: Username to create
174 :param str password: Password for user; if ``None``, create a system user
175 :param str shell: The default shell for the user
176 :param bool system_user: Whether to create a login or system user
177 :param str primary_group: Primary group for user; defaults to their username
178 :param list secondary_groups: Optional list of additional groups
179
180 :returns: The password database entry struct, as returned by `pwd.getpwnam`
181 """
131 try:182 try:
132 user_info = pwd.getpwnam(username)183 user_info = pwd.getpwnam(username)
133 log('user {0} already exists!'.format(username))184 log('user {0} already exists!'.format(username))
@@ -142,6 +193,16 @@
142 '--shell', shell,193 '--shell', shell,
143 '--password', password,194 '--password', password,
144 ])195 ])
196 if not primary_group:
197 try:
198 grp.getgrnam(username)
199 primary_group = username # avoid "group exists" error
200 except KeyError:
201 pass
202 if primary_group:
203 cmd.extend(['-g', primary_group])
204 if secondary_groups:
205 cmd.extend(['-G', ','.join(secondary_groups)])
145 cmd.append(username)206 cmd.append(username)
146 subprocess.check_call(cmd)207 subprocess.check_call(cmd)
147 user_info = pwd.getpwnam(username)208 user_info = pwd.getpwnam(username)
@@ -550,7 +611,14 @@
550 os.chdir(cur)611 os.chdir(cur)
551612
552613
553def chownr(path, owner, group, follow_links=True):614def chownr(path, owner, group, follow_links=True, chowntopdir=False):
615 """
616 Recursively change user and group ownership of files and directories
617 in given path. Doesn't chown path itself by default, only its children.
618
619 :param bool follow_links: Also Chown links if True
620 :param bool chowntopdir: Also chown path itself if True
621 """
554 uid = pwd.getpwnam(owner).pw_uid622 uid = pwd.getpwnam(owner).pw_uid
555 gid = grp.getgrnam(group).gr_gid623 gid = grp.getgrnam(group).gr_gid
556 if follow_links:624 if follow_links:
@@ -558,6 +626,10 @@
558 else:626 else:
559 chown = os.lchown627 chown = os.lchown
560628
629 if chowntopdir:
630 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
631 if not broken_symlink:
632 chown(path, uid, gid)
561 for root, dirs, files in os.walk(path):633 for root, dirs, files in os.walk(path):
562 for name in dirs + files:634 for name in dirs + files:
563 full = os.path.join(root, name)635 full = os.path.join(root, name)
@@ -568,3 +640,19 @@
568640
569def lchownr(path, owner, group):641def lchownr(path, owner, group):
570 chownr(path, owner, group, follow_links=False)642 chownr(path, owner, group, follow_links=False)
643
644
645def get_total_ram():
646 '''The total amount of system RAM in bytes.
647
648 This is what is reported by the OS, and may be overcommitted when
649 there are multiple containers hosted on the same machine.
650 '''
651 with open('/proc/meminfo', 'r') as f:
652 for line in f.readlines():
653 if line:
654 key, value, unit = line.split()
655 if key == 'MemTotal:':
656 assert unit == 'kB', 'Unknown unit'
657 return int(value) * 1024 # Classic, not KiB.
658 raise NotImplementedError()
571659
=== added file 'hooks/charmhelpers/core/hugepage.py'
--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/hugepage.py 2016-02-18 14:28:13 +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
=== removed file 'hooks/charmhelpers/core/hugepage.py'
--- hooks/charmhelpers/core/hugepage.py 2015-08-19 13:51:03 +0000
+++ hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
@@ -1,62 +0,0 @@
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)
28
29
30def hugepage_support(user, group='hugetlb', nr_hugepages=256,
31 max_map_count=65536, mnt_point='/run/hugepages/kvm',
32 pagesize='2MB', mount=True):
33 """Enable hugepages on system.
34
35 Args:
36 user (str) -- Username to allow access to hugepages to
37 group (str) -- Group name to own hugepages
38 nr_hugepages (int) -- Number of pages to reserve
39 max_map_count (int) -- Number of Virtual Memory Areas a process can own
40 mnt_point (str) -- Directory to mount hugepages on
41 pagesize (str) -- Size of hugepages
42 mount (bool) -- Whether to Mount hugepages
43 """
44 group_info = add_group(group)
45 gid = group_info.gr_gid
46 add_user_to_group(user, group)
47 sysctl_settings = {
48 'vm.nr_hugepages': nr_hugepages,
49 'vm.max_map_count': max_map_count,
50 'vm.hugetlb_shm_group': gid,
51 }
52 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
53 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
54 lfstab = fstab.Fstab()
55 fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
56 if fstab_entry:
57 lfstab.remove_entry(fstab_entry)
58 entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
59 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
60 lfstab.add_entry(entry)
61 if mount:
62 fstab_mount(mnt_point)
630
=== added file 'hooks/charmhelpers/core/kernel.py'
--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel.py 2016-02-18 14:28:13 +0000
@@ -0,0 +1,68 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO
25)
26
27from subprocess import check_call, check_output
28import re
29
30
31def modprobe(module, persist=True):
32 """Load a kernel module and configure for auto-load on reboot."""
33 cmd = ['modprobe', module]
34
35 log('Loading kernel module %s' % module, level=INFO)
36
37 check_call(cmd)
38 if persist:
39 with open('/etc/modules', 'r+') as modules:
40 if module not in modules.read():
41 modules.write(module)
42
43
44def rmmod(module, force=False):
45 """Remove a module from the linux kernel"""
46 cmd = ['rmmod']
47 if force:
48 cmd.append('-f')
49 cmd.append(module)
50 log('Removing kernel module %s' % module, level=INFO)
51 return check_call(cmd)
52
53
54def lsmod():
55 """Shows what kernel modules are currently loaded"""
56 return check_output(['lsmod'],
57 universal_newlines=True)
58
59
60def is_module_loaded(module):
61 """Checks if a kernel module is already loaded"""
62 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
63 return len(matches) > 0
64
65
66def update_initramfs(version='all'):
67 """Updates an initramfs image"""
68 return check_call(["update-initramfs", "-k", version, "-u"])
069
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2015-08-18 17:34:36 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2016-02-18 14:28:13 +0000
@@ -243,33 +243,40 @@
243 :param str source: The template source file, relative to243 :param str source: The template source file, relative to
244 `$CHARM_DIR/templates`244 `$CHARM_DIR/templates`
245245
246 :param str target: The target to write the rendered template to246 :param str target: The target to write the rendered template to (or None)
247 :param str owner: The owner of the rendered file247 :param str owner: The owner of the rendered file
248 :param str group: The group of the rendered file248 :param str group: The group of the rendered file
249 :param int perms: The permissions of the rendered file249 :param int perms: The permissions of the rendered file
250 :param partial on_change_action: functools partial to be executed when250 :param partial on_change_action: functools partial to be executed when
251 rendered file changes251 rendered file changes
252 :param jinja2 loader template_loader: A jinja2 template loader
253
254 :return str: The rendered template
252 """255 """
253 def __init__(self, source, target,256 def __init__(self, source, target,
254 owner='root', group='root', perms=0o444,257 owner='root', group='root', perms=0o444,
255 on_change_action=None):258 on_change_action=None, template_loader=None):
256 self.source = source259 self.source = source
257 self.target = target260 self.target = target
258 self.owner = owner261 self.owner = owner
259 self.group = group262 self.group = group
260 self.perms = perms263 self.perms = perms
261 self.on_change_action = on_change_action264 self.on_change_action = on_change_action
265 self.template_loader = template_loader
262266
263 def __call__(self, manager, service_name, event_name):267 def __call__(self, manager, service_name, event_name):
264 pre_checksum = ''268 pre_checksum = ''
265 if self.on_change_action and os.path.isfile(self.target):269 if self.on_change_action and os.path.isfile(self.target):
266 pre_checksum = host.file_hash(self.target)270 pre_checksum = host.file_hash(self.target)
267 service = manager.get_service(service_name)271 service = manager.get_service(service_name)
268 context = {}272 context = {'ctx': {}}
269 for ctx in service.get('required_data', []):273 for ctx in service.get('required_data', []):
270 context.update(ctx)274 context.update(ctx)
271 templating.render(self.source, self.target, context,275 context['ctx'].update(ctx)
272 self.owner, self.group, self.perms)276
277 result = templating.render(self.source, self.target, context,
278 self.owner, self.group, self.perms,
279 template_loader=self.template_loader)
273 if self.on_change_action:280 if self.on_change_action:
274 if pre_checksum == host.file_hash(self.target):281 if pre_checksum == host.file_hash(self.target):
275 hookenv.log(282 hookenv.log(
@@ -278,6 +285,8 @@
278 else:285 else:
279 self.on_change_action()286 self.on_change_action()
280287
288 return result
289
281290
282# Convenience aliases for templates291# Convenience aliases for templates
283render_template = template = TemplateCallback292render_template = template = TemplateCallback
284293
=== modified file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 2015-04-16 20:24:28 +0000
+++ hooks/charmhelpers/core/strutils.py 2016-02-18 14:28:13 +0000
@@ -18,6 +18,7 @@
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20import six20import six
21import re
2122
2223
23def bool_from_string(value):24def bool_from_string(value):
@@ -40,3 +41,32 @@
4041
41 msg = "Unable to interpret string value '%s' as boolean" % (value)42 msg = "Unable to interpret string value '%s' as boolean" % (value)
42 raise ValueError(msg)43 raise ValueError(msg)
44
45
46def bytes_from_string(value):
47 """Interpret human readable string value as bytes.
48
49 Returns int
50 """
51 BYTE_POWER = {
52 'K': 1,
53 'KB': 1,
54 'M': 2,
55 'MB': 2,
56 'G': 3,
57 'GB': 3,
58 'T': 4,
59 'TB': 4,
60 'P': 5,
61 'PB': 5,
62 }
63 if isinstance(value, six.string_types):
64 value = six.text_type(value)
65 else:
66 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
67 raise ValueError(msg)
68 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
69 if not matches:
70 msg = "Unable to interpret string value '%s' as bytes" % (value)
71 raise ValueError(msg)
72 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
4373
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2015-02-26 10:11:26 +0000
+++ hooks/charmhelpers/core/templating.py 2016-02-18 14:28:13 +0000
@@ -21,13 +21,14 @@
2121
2222
23def render(source, target, context, owner='root', group='root',23def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None, encoding='UTF-8'):24 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
25 """25 """
26 Render a template.26 Render a template.
2727
28 The `source` path, if not absolute, is relative to the `templates_dir`.28 The `source` path, if not absolute, is relative to the `templates_dir`.
2929
30 The `target` path should be absolute.30 The `target` path should be absolute. It can also be `None`, in which
31 case no file will be written.
3132
32 The context should be a dict containing the values to be replaced in the33 The context should be a dict containing the values to be replaced in the
33 template.34 template.
@@ -36,6 +37,9 @@
3637
37 If omitted, `templates_dir` defaults to the `templates` folder in the charm.38 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
3839
40 The rendered template will be written to the file as well as being returned
41 as a string.
42
39 Note: Using this requires python-jinja2; if it is not installed, calling43 Note: Using this requires python-jinja2; if it is not installed, calling
40 this will attempt to use charmhelpers.fetch.apt_install to install it.44 this will attempt to use charmhelpers.fetch.apt_install to install it.
41 """45 """
@@ -52,17 +56,26 @@
52 apt_install('python-jinja2', fatal=True)56 apt_install('python-jinja2', fatal=True)
53 from jinja2 import FileSystemLoader, Environment, exceptions57 from jinja2 import FileSystemLoader, Environment, exceptions
5458
55 if templates_dir is None:59 if template_loader:
56 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')60 template_env = Environment(loader=template_loader)
57 loader = Environment(loader=FileSystemLoader(templates_dir))61 else:
62 if templates_dir is None:
63 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
64 template_env = Environment(loader=FileSystemLoader(templates_dir))
58 try:65 try:
59 source = source66 source = source
60 template = loader.get_template(source)67 template = template_env.get_template(source)
61 except exceptions.TemplateNotFound as e:68 except exceptions.TemplateNotFound as e:
62 hookenv.log('Could not load template %s from %s.' %69 hookenv.log('Could not load template %s from %s.' %
63 (source, templates_dir),70 (source, templates_dir),
64 level=hookenv.ERROR)71 level=hookenv.ERROR)
65 raise e72 raise e
66 content = template.render(context)73 content = template.render(context)
67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)74 if target is not None:
68 host.write_file(target, content.encode(encoding), owner, group, perms)75 target_dir = os.path.dirname(target)
76 if not os.path.exists(target_dir):
77 # This is a terrible default directory permission, as the file
78 # or its siblings will often contain secrets.
79 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
80 host.write_file(target, content.encode(encoding), owner, group, perms)
81 return content
6982
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-08-18 17:34:36 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2016-02-18 14:28:13 +0000
@@ -98,6 +98,14 @@
98 'liberty/proposed': 'trusty-proposed/liberty',98 'liberty/proposed': 'trusty-proposed/liberty',
99 'trusty-liberty/proposed': 'trusty-proposed/liberty',99 'trusty-liberty/proposed': 'trusty-proposed/liberty',
100 'trusty-proposed/liberty': 'trusty-proposed/liberty',100 'trusty-proposed/liberty': 'trusty-proposed/liberty',
101 # Mitaka
102 'mitaka': 'trusty-updates/mitaka',
103 'trusty-mitaka': 'trusty-updates/mitaka',
104 'trusty-mitaka/updates': 'trusty-updates/mitaka',
105 'trusty-updates/mitaka': 'trusty-updates/mitaka',
106 'mitaka/proposed': 'trusty-proposed/mitaka',
107 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
108 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
101}109}
102110
103# The order of this list is very important. Handlers should be listed in from111# The order of this list is very important. Handlers should be listed in from
@@ -225,12 +233,12 @@
225233
226def apt_mark(packages, mark, fatal=False):234def apt_mark(packages, mark, fatal=False):
227 """Flag one or more packages using apt-mark"""235 """Flag one or more packages using apt-mark"""
236 log("Marking {} as {}".format(packages, mark))
228 cmd = ['apt-mark', mark]237 cmd = ['apt-mark', mark]
229 if isinstance(packages, six.string_types):238 if isinstance(packages, six.string_types):
230 cmd.append(packages)239 cmd.append(packages)
231 else:240 else:
232 cmd.extend(packages)241 cmd.extend(packages)
233 log("Holding {}".format(packages))
234242
235 if fatal:243 if fatal:
236 subprocess.check_call(cmd, universal_newlines=True)244 subprocess.check_call(cmd, universal_newlines=True)
@@ -411,7 +419,7 @@
411 importlib.import_module(package),419 importlib.import_module(package),
412 classname)420 classname)
413 plugin_list.append(handler_class())421 plugin_list.append(handler_class())
414 except (ImportError, AttributeError):422 except NotImplementedError:
415 # Skip missing plugins so that they can be ommitted from423 # Skip missing plugins so that they can be ommitted from
416 # installation if desired424 # installation if desired
417 log("FetchHandler {} not found, skipping plugin".format(425 log("FetchHandler {} not found, skipping plugin".format(
418426
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2015-07-17 13:24:05 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2016-02-18 14:28:13 +0000
@@ -108,7 +108,7 @@
108 install_opener(opener)108 install_opener(opener)
109 response = urlopen(source)109 response = urlopen(source)
110 try:110 try:
111 with open(dest, 'w') as dest_file:111 with open(dest, 'wb') as dest_file:
112 dest_file.write(response.read())112 dest_file.write(response.read())
113 except Exception as e:113 except Exception as e:
114 if os.path.isfile(dest):114 if os.path.isfile(dest):
115115
=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-26 09:46:38 +0000
+++ hooks/charmhelpers/fetch/bzrurl.py 2016-02-18 14:28:13 +0000
@@ -15,60 +15,50 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from subprocess import check_call
18from charmhelpers.fetch import (19from charmhelpers.fetch import (
19 BaseFetchHandler,20 BaseFetchHandler,
20 UnhandledSource21 UnhandledSource,
22 filter_installed_packages,
23 apt_install,
21)24)
22from charmhelpers.core.host import mkdir25from charmhelpers.core.host import mkdir
2326
24import six
25if six.PY3:
26 raise ImportError('bzrlib does not support Python3')
2727
28try:28if filter_installed_packages(['bzr']) != []:
29 from bzrlib.branch import Branch29 apt_install(['bzr'])
30 from bzrlib import bzrdir, workingtree, errors30 if filter_installed_packages(['bzr']) != []:
31except ImportError:31 raise NotImplementedError('Unable to install bzr')
32 from charmhelpers.fetch import apt_install
33 apt_install("python-bzrlib")
34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
3632
3733
38class BzrUrlFetchHandler(BaseFetchHandler):34class BzrUrlFetchHandler(BaseFetchHandler):
39 """Handler for bazaar branches via generic and lp URLs"""35 """Handler for bazaar branches via generic and lp URLs"""
40 def can_handle(self, source):36 def can_handle(self, source):
41 url_parts = self.parse_url(source)37 url_parts = self.parse_url(source)
42 if url_parts.scheme not in ('bzr+ssh', 'lp'):38 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
43 return False39 return False
40 elif not url_parts.scheme:
41 return os.path.exists(os.path.join(source, '.bzr'))
44 else:42 else:
45 return True43 return True
4644
47 def branch(self, source, dest):45 def branch(self, source, dest):
48 url_parts = self.parse_url(source)
49 # If we use lp:branchname scheme we need to load plugins
50 if not self.can_handle(source):46 if not self.can_handle(source):
51 raise UnhandledSource("Cannot handle {}".format(source))47 raise UnhandledSource("Cannot handle {}".format(source))
52 if url_parts.scheme == "lp":48 if os.path.exists(dest):
53 from bzrlib.plugin import load_plugins49 check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
54 load_plugins()50 else:
55 try:51 check_call(['bzr', 'branch', source, dest])
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
57 except errors.AlreadyControlDirError:
58 local_branch = Branch.open(dest)
59 try:
60 remote_branch = Branch.open(source)
61 remote_branch.push(local_branch)
62 tree = workingtree.WorkingTree.open(dest)
63 tree.update()
64 except Exception as e:
65 raise e
6652
67 def install(self, source):53 def install(self, source, dest=None):
68 url_parts = self.parse_url(source)54 url_parts = self.parse_url(source)
69 branch_name = url_parts.path.strip("/").split("/")[-1]55 branch_name = url_parts.path.strip("/").split("/")[-1]
70 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",56 if dest:
71 branch_name)57 dest_dir = os.path.join(dest, branch_name)
58 else:
59 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
60 branch_name)
61
72 if not os.path.exists(dest_dir):62 if not os.path.exists(dest_dir):
73 mkdir(dest_dir, perms=0o755)63 mkdir(dest_dir, perms=0o755)
74 try:64 try:
7565
=== modified file 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2015-07-17 13:24:05 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2016-02-18 14:28:13 +0000
@@ -15,24 +15,18 @@
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import os17import os
18from subprocess import check_call
18from charmhelpers.fetch import (19from charmhelpers.fetch import (
19 BaseFetchHandler,20 BaseFetchHandler,
20 UnhandledSource21 UnhandledSource,
22 filter_installed_packages,
23 apt_install,
21)24)
22from charmhelpers.core.host import mkdir25
2326if filter_installed_packages(['git']) != []:
24import six27 apt_install(['git'])
25if six.PY3:28 if filter_installed_packages(['git']) != []:
26 raise ImportError('GitPython does not support Python 3')29 raise NotImplementedError('Unable to install git')
27
28try:
29 from git import Repo
30except ImportError:
31 from charmhelpers.fetch import apt_install
32 apt_install("python-git")
33 from git import Repo
34
35from git.exc import GitCommandError # noqa E402
3630
3731
38class GitUrlFetchHandler(BaseFetchHandler):32class GitUrlFetchHandler(BaseFetchHandler):
@@ -40,19 +34,24 @@
40 def can_handle(self, source):34 def can_handle(self, source):
41 url_parts = self.parse_url(source)35 url_parts = self.parse_url(source)
42 # TODO (mattyw) no support for ssh git@ yet36 # TODO (mattyw) no support for ssh git@ yet
43 if url_parts.scheme not in ('http', 'https', 'git'):37 if url_parts.scheme not in ('http', 'https', 'git', ''):
44 return False38 return False
39 elif not url_parts.scheme:
40 return os.path.exists(os.path.join(source, '.git'))
45 else:41 else:
46 return True42 return True
4743
48 def clone(self, source, dest, branch, depth=None):44 def clone(self, source, dest, branch="master", depth=None):
49 if not self.can_handle(source):45 if not self.can_handle(source):
50 raise UnhandledSource("Cannot handle {}".format(source))46 raise UnhandledSource("Cannot handle {}".format(source))
5147
48 if os.path.exists(dest):
49 cmd = ['git', '-C', dest, 'pull', source, branch]
50 else:
51 cmd = ['git', 'clone', source, dest, '--branch', branch]
52 if depth:52 if depth:
53 Repo.clone_from(source, dest, branch=branch, depth=depth)53 cmd.extend(['--depth', depth])
54 else:54 check_call(cmd)
55 Repo.clone_from(source, dest, branch=branch)
5655
57 def install(self, source, branch="master", dest=None, depth=None):56 def install(self, source, branch="master", dest=None, depth=None):
58 url_parts = self.parse_url(source)57 url_parts = self.parse_url(source)
@@ -62,12 +61,8 @@
62 else:61 else:
63 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",62 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
64 branch_name)63 branch_name)
65 if not os.path.exists(dest_dir):
66 mkdir(dest_dir, perms=0o755)
67 try:64 try:
68 self.clone(source, dest_dir, branch, depth)65 self.clone(source, dest_dir, branch, depth)
69 except GitCommandError as e:
70 raise UnhandledSource(e)
71 except OSError as e:66 except OSError as e:
72 raise UnhandledSource(e.strerror)67 raise UnhandledSource(e.strerror)
73 return dest_dir68 return dest_dir
7469
=== added file 'hooks/charmhelpers/payload/archive.py'
--- hooks/charmhelpers/payload/archive.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/payload/archive.py 2016-02-18 14:28:13 +0000
@@ -0,0 +1,73 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import tarfile
19import zipfile
20from charmhelpers.core import (
21 host,
22 hookenv,
23)
24
25
26class ArchiveError(Exception):
27 pass
28
29
30def get_archive_handler(archive_name):
31 if os.path.isfile(archive_name):
32 if tarfile.is_tarfile(archive_name):
33 return extract_tarfile
34 elif zipfile.is_zipfile(archive_name):
35 return extract_zipfile
36 else:
37 # look at the file name
38 for ext in ('.tar', '.tar.gz', '.tgz', 'tar.bz2', '.tbz2', '.tbz'):
39 if archive_name.endswith(ext):
40 return extract_tarfile
41 for ext in ('.zip', '.jar'):
42 if archive_name.endswith(ext):
43 return extract_zipfile
44
45
46def archive_dest_default(archive_name):
47 archive_file = os.path.basename(archive_name)
48 return os.path.join(hookenv.charm_dir(), "archives", archive_file)
49
50
51def extract(archive_name, destpath=None):
52 handler = get_archive_handler(archive_name)
53 if handler:
54 if not destpath:
55 destpath = archive_dest_default(archive_name)
56 if not os.path.isdir(destpath):
57 host.mkdir(destpath)
58 handler(archive_name, destpath)
59 return destpath
60 else:
61 raise ArchiveError("No handler for archive")
62
63
64def extract_tarfile(archive_name, destpath):
65 "Unpack a tar archive, optionally compressed"
66 archive = tarfile.open(archive_name)
67 archive.extractall(destpath)
68
69
70def extract_zipfile(archive_name, destpath):
71 "Unpack a zip file"
72 archive = zipfile.ZipFile(archive_name)
73 archive.extractall(destpath)
074
=== added symlink 'hooks/dashboard-plugin-relation-changed'
=== target is u'horizon_hooks.py'
=== removed symlink 'hooks/dashboard-plugin-relation-changed'
=== target was u'horizon_hooks.py'
=== added symlink 'hooks/dashboard-plugin-relation-joined'
=== target is u'horizon_hooks.py'
=== removed symlink 'hooks/dashboard-plugin-relation-joined'
=== target was u'horizon_hooks.py'
=== modified file 'hooks/horizon_hooks.py'
--- hooks/horizon_hooks.py 2015-09-28 19:15:37 +0000
+++ hooks/horizon_hooks.py 2016-02-18 14:28:13 +0000
@@ -10,7 +10,8 @@
10 relation_set,10 relation_set,
11 relation_get,11 relation_get,
12 relation_ids,12 relation_ids,
13 unit_get13 unit_get,
14 status_set,
14)15)
15from charmhelpers.fetch import (16from charmhelpers.fetch import (
16 apt_update, apt_install,17 apt_update, apt_install,
@@ -27,7 +28,8 @@
27 git_pip_venv_dir,28 git_pip_venv_dir,
28 openstack_upgrade_available,29 openstack_upgrade_available,
29 os_release,30 os_release,
30 save_script_rc31 save_script_rc,
32 set_os_workload_status,
31)33)
32from horizon_utils import (34from horizon_utils import (
33 determine_packages,35 determine_packages,
@@ -40,7 +42,8 @@
40 git_install,42 git_install,
41 git_post_install_late,43 git_post_install_late,
42 setup_ipv6,44 setup_ipv6,
43 INSTALL_DIR45 INSTALL_DIR,
46 REQUIRED_INTERFACES,
44)47)
45from charmhelpers.contrib.network.ip import (48from charmhelpers.contrib.network.ip import (
46 get_iface_for_address,49 get_iface_for_address,
@@ -70,7 +73,10 @@
70 if lsb_release()['DISTRIB_CODENAME'] == 'precise':73 if lsb_release()['DISTRIB_CODENAME'] == 'precise':
71 # Explicitly upgrade python-six Bug#142070874 # Explicitly upgrade python-six Bug#1420708
72 apt_install('python-six', fatal=True)75 apt_install('python-six', fatal=True)
73 apt_install(filter_installed_packages(packages), fatal=True)76 packages = filter_installed_packages(packages)
77 if packages:
78 status_set('maintenance', 'Installing packages')
79 apt_install(packages, fatal=True)
7480
75 git_install(config('openstack-origin-git'))81 git_install(config('openstack-origin-git'))
7682
@@ -108,6 +114,7 @@
108 git_install(config('openstack-origin-git'))114 git_install(config('openstack-origin-git'))
109 elif not config('action-managed-upgrade'):115 elif not config('action-managed-upgrade'):
110 if openstack_upgrade_available('openstack-dashboard'):116 if openstack_upgrade_available('openstack-dashboard'):
117 status_set('maintenance', 'Upgrading to new OpenStack release')
111 do_openstack_upgrade(configs=CONFIGS)118 do_openstack_upgrade(configs=CONFIGS)
112119
113 env_vars = {120 env_vars = {
@@ -265,10 +272,12 @@
265272
266273
267def main():274def main():
275 print sys.argv
268 try:276 try:
269 hooks.execute(sys.argv)277 hooks.execute(sys.argv)
270 except UnregisteredHookError as e:278 except UnregisteredHookError as e:
271 log('Unknown hook {} - skipping.'.format(e))279 log('Unknown hook {} - skipping.'.format(e))
280 set_os_workload_status(CONFIGS, REQUIRED_INTERFACES)
272281
273282
274if __name__ == '__main__':283if __name__ == '__main__':
275284
=== modified file 'hooks/horizon_utils.py'
--- hooks/horizon_utils.py 2015-09-28 19:15:37 +0000
+++ hooks/horizon_utils.py 2016-02-18 14:28:13 +0000
@@ -72,6 +72,9 @@
72 'zlib1g-dev',72 'zlib1g-dev',
73]73]
7474
75REQUIRED_INTERFACES = {
76 'identity': ['identity-service'],
77}
75# ubuntu packages that should not be installed when deploying from git78# ubuntu packages that should not be installed when deploying from git
76GIT_PACKAGE_BLACKLIST = [79GIT_PACKAGE_BLACKLIST = [
77 'openstack-dashboard',80 'openstack-dashboard',
7881
=== added symlink 'hooks/identity-service-relation-departed'
=== target is u'horizon_hooks.py'
=== added symlink 'hooks/install.real'
=== target is u'horizon_hooks.py'
=== removed symlink 'hooks/install.real'
=== target was u'horizon_hooks.py'
=== modified file 'metadata.yaml'
--- metadata.yaml 2015-09-30 13:56:20 +0000
+++ metadata.yaml 2016-02-18 14:28:13 +0000
@@ -1,6 +1,6 @@
1name: openstack-dashboard1name: openstack-dashboard
2summary: a Django web interface to OpenStack2summary: Web dashboard for OpenStack
3maintainer: Adam Gandelman <adamg@canonical.com>3maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
4description: |4description: |
5 The OpenStack Dashboard provides a full feature web interface for interacting5 The OpenStack Dashboard provides a full feature web interface for interacting
6 with instances, images, volumes and networks within an OpenStack deployment.6 with instances, images, volumes and networks within an OpenStack deployment.
77
=== modified file 'templates/icehouse/local_settings.py'
--- templates/icehouse/local_settings.py 2015-09-25 02:05:05 +0000
+++ templates/icehouse/local_settings.py 2016-02-18 14:28:13 +0000
@@ -213,7 +213,7 @@
213# external to the OpenStack environment. The default is 'publicURL'.213# external to the OpenStack environment. The default is 'publicURL'.
214#OPENSTACK_ENDPOINT_TYPE = "publicURL"214#OPENSTACK_ENDPOINT_TYPE = "publicURL"
215{% if primary_endpoint -%}215{% if primary_endpoint -%}
216OPENSTACK_ENDPOINT_TYPE = {{ primary_endpoint }}216OPENSTACK_ENDPOINT_TYPE = "{{ primary_endpoint }}"
217{% endif -%}217{% endif -%}
218218
219# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the219# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the
@@ -223,7 +223,7 @@
223# value should differ from OPENSTACK_ENDPOINT_TYPE if used.223# value should differ from OPENSTACK_ENDPOINT_TYPE if used.
224#SECONDARY_ENDPOINT_TYPE = "publicURL"224#SECONDARY_ENDPOINT_TYPE = "publicURL"
225{% if secondary_endpoint -%}225{% if secondary_endpoint -%}
226SECONDARY_ENDPOINT_TYPE = {{ secondary_endpoint }}226SECONDARY_ENDPOINT_TYPE = "{{ secondary_endpoint }}"
227{% endif -%}227{% endif -%}
228228
229# The number of objects (Swift containers/objects or images) to display229# The number of objects (Swift containers/objects or images) to display
@@ -521,4 +521,4 @@
521# see https://docs.djangoproject.com/en/dev/ref/settings/.521# see https://docs.djangoproject.com/en/dev/ref/settings/.
522ALLOWED_HOSTS = '*'522ALLOWED_HOSTS = '*'
523523
524{{ settings|join('\n\n') }}
525\ No newline at end of file524\ No newline at end of file
525{{ settings|join('\n\n') }}
526526
=== modified file 'templates/juno/local_settings.py'
--- templates/juno/local_settings.py 2015-09-25 02:05:05 +0000
+++ templates/juno/local_settings.py 2016-02-18 14:28:13 +0000
@@ -251,7 +251,7 @@
251# external to the OpenStack environment. The default is 'publicURL'.251# external to the OpenStack environment. The default is 'publicURL'.
252#OPENSTACK_ENDPOINT_TYPE = "publicURL"252#OPENSTACK_ENDPOINT_TYPE = "publicURL"
253{% if primary_endpoint -%}253{% if primary_endpoint -%}
254OPENSTACK_ENDPOINT_TYPE = {{ primary_endpoint }}254OPENSTACK_ENDPOINT_TYPE = "{{ primary_endpoint }}"
255{% endif -%}255{% endif -%}
256256
257# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the257# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the
@@ -261,7 +261,7 @@
261# value should differ from OPENSTACK_ENDPOINT_TYPE if used.261# value should differ from OPENSTACK_ENDPOINT_TYPE if used.
262#SECONDARY_ENDPOINT_TYPE = "publicURL"262#SECONDARY_ENDPOINT_TYPE = "publicURL"
263{% if secondary_endpoint -%}263{% if secondary_endpoint -%}
264SECONDARY_ENDPOINT_TYPE = {{ secondary_endpoint }}264SECONDARY_ENDPOINT_TYPE = "{{ secondary_endpoint }}"
265{% endif -%}265{% endif -%}
266266
267# The number of objects (Swift containers/objects or images) to display267# The number of objects (Swift containers/objects or images) to display
@@ -626,4 +626,4 @@
626# see https://docs.djangoproject.com/en/dev/ref/settings/.626# see https://docs.djangoproject.com/en/dev/ref/settings/.
627ALLOWED_HOSTS = '*'627ALLOWED_HOSTS = '*'
628628
629{{ settings|join('\n\n') }}
630\ No newline at end of file629\ No newline at end of file
630{{ settings|join('\n\n') }}
631631
=== added file 'tests/018-basic-trusty-liberty'
--- tests/018-basic-trusty-liberty 1970-01-01 00:00:00 +0000
+++ tests/018-basic-trusty-liberty 2016-02-18 14:28:13 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic openstack-dashboard deployment on trusty-liberty."""
4
5from basic_deployment import OpenstackDashboardBasicDeployment
6
7if __name__ == '__main__':
8 deployment = OpenstackDashboardBasicDeployment(series='trusty',
9 openstack='cloud:trusty-liberty',
10 source='cloud:trusty-updates/liberty')
11 deployment.run_tests()
012
=== added file 'tests/019-basic-trusty-mitaka'
--- tests/019-basic-trusty-mitaka 1970-01-01 00:00:00 +0000
+++ tests/019-basic-trusty-mitaka 2016-02-18 14:28:13 +0000
@@ -0,0 +1,11 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic openstack-dashboard deployment on trusty-mitaka."""
4
5from basic_deployment import OpenstackDashboardBasicDeployment
6
7if __name__ == '__main__':
8 deployment = OpenstackDashboardBasicDeployment(series='trusty',
9 openstack='cloud:trusty-mitaka',
10 source='cloud:trusty-updates/mitaka')
11 deployment.run_tests()
012
=== added file 'tests/020-basic-wily-liberty'
--- tests/020-basic-wily-liberty 1970-01-01 00:00:00 +0000
+++ tests/020-basic-wily-liberty 2016-02-18 14:28:13 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2
3"""Amulet tests on a basic openstack-dashboard deployment on wily-liberty."""
4
5from basic_deployment import OpenstackDashboardBasicDeployment
6
7if __name__ == '__main__':
8 deployment = OpenstackDashboardBasicDeployment(series='wily')
9 deployment.run_tests()
010
=== added file 'tests/021-basic-xenial-mitaka'
--- tests/021-basic-xenial-mitaka 1970-01-01 00:00:00 +0000
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: